diff --git a/.editorconfig b/.editorconfig index fd68808456..71dd40de98 100644 --- a/.editorconfig +++ b/.editorconfig @@ -71,10 +71,10 @@ dotnet_naming_symbols.any_async_methods.applicable_kinds = method dotnet_naming_symbols.any_async_methods.applicable_accessibilities = * dotnet_naming_symbols.any_async_methods.required_modifiers = async -dotnet_naming_style.end_in_async.required_prefix = +dotnet_naming_style.end_in_async.required_prefix = dotnet_naming_style.end_in_async.required_suffix = Async dotnet_naming_style.end_in_async.capitalization = pascal_case -dotnet_naming_style.end_in_async.word_separator = +dotnet_naming_style.end_in_async.word_separator = # Obsolete warnings, this should be removed or changed to warning once we address some of the obsolete items. dotnet_diagnostic.CS0618.severity = suggestion @@ -85,6 +85,12 @@ dotnet_diagnostic.CS0612.severity = suggestion # Remove unnecessary using directives https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0005 dotnet_diagnostic.IDE0005.severity = warning +# Specify CultureInfo https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1304 +dotnet_diagnostic.CA1304.severity = warning + +# Specify IFormatProvider https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1305 +dotnet_diagnostic.CA1305.severity = warning + # CSharp code style settings: [*.cs] # Prefer "var" everywhere diff --git a/.vscode/launch.json b/.vscode/launch.json index c407ba5604..74115dcc86 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -69,6 +69,28 @@ "preLaunchTask": "buildFullServer", "stopAll": true }, + { + "name": "Full Server with Seeder API", + "configurations": [ + "run-Admin", + "run-API", + "run-Events", + "run-EventsProcessor", + "run-Identity", + "run-Sso", + "run-Icons", + "run-Billing", + "run-Notifications", + "run-SeederAPI" + ], + "presentation": { + "hidden": false, + "group": "AA_compounds", + "order": 6 + }, + "preLaunchTask": "buildFullServerWithSeederApi", + "stopAll": true + }, { "name": "Self Host: Bit", "configurations": [ @@ -204,6 +226,17 @@ }, "preLaunchTask": "buildSso", }, + { + "name": "Seeder API", + "configurations": [ + "run-SeederAPI" + ], + "presentation": { + "hidden": false, + "group": "cloud", + }, + "preLaunchTask": "buildSeederAPI", + }, { "name": "Admin Self Host", "configurations": [ @@ -270,6 +303,17 @@ }, "preLaunchTask": "buildSso", }, + { + "name": "Seeder API Self Host", + "configurations": [ + "run-SeederAPI-SelfHost" + ], + "presentation": { + "hidden": false, + "group": "self-host", + }, + "preLaunchTask": "buildSeederAPI", + } ], "configurations": [ // Configurations represent run-only scenarios so that they can be used in multiple compounds @@ -311,6 +355,25 @@ "/Views": "${workspaceFolder}/Views" } }, + { + "name": "run-SeederAPI", + "presentation": { + "hidden": true, + }, + "requireExactSource": true, + "type": "coreclr", + "request": "launch", + "program": "${workspaceFolder}/util/SeederApi/bin/Debug/net8.0/SeederApi.dll", + "args": [], + "cwd": "${workspaceFolder}/util/SeederApi", + "stopAtEntry": false, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development", + }, + "sourceFileMap": { + "/Views": "${workspaceFolder}/Views" + } + }, { "name": "run-Billing", "presentation": { @@ -488,6 +551,27 @@ "/Views": "${workspaceFolder}/Views" } }, + { + "name": "run-SeederAPI-SelfHost", + "presentation": { + "hidden": true, + }, + "requireExactSource": true, + "type": "coreclr", + "request": "launch", + "program": "${workspaceFolder}/util/SeederApi/bin/Debug/net8.0/SeederApi.dll", + "args": [], + "cwd": "${workspaceFolder}/util/SeederApi", + "stopAtEntry": false, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development", + "ASPNETCORE_URLS": "http://localhost:5048", + "developSelfHosted": "true", + }, + "sourceFileMap": { + "/Views": "${workspaceFolder}/Views" + } + }, { "name": "run-Admin-SelfHost", "presentation": { diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 567f9b6e58..07a55fdeb3 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -43,6 +43,21 @@ "label": "buildFullServer", "hide": true, "dependsOrder": "sequence", + "dependsOn": [ + "buildAdmin", + "buildAPI", + "buildEventsProcessor", + "buildIdentity", + "buildSso", + "buildIcons", + "buildBilling", + "buildNotifications" + ], + }, + { + "label": "buildFullServerWithSeederApi", + "hide": true, + "dependsOrder": "sequence", "dependsOn": [ "buildAdmin", "buildAPI", @@ -52,6 +67,7 @@ "buildIcons", "buildBilling", "buildNotifications", + "buildSeederAPI" ], }, { @@ -89,6 +105,9 @@ "/property:GenerateFullPaths=true", "/consoleloggerparameters:NoSummary" ], + "options": { + "cwd": "${workspaceFolder}" + }, "problemMatcher": "$msCompile" }, { @@ -102,6 +121,9 @@ "/property:GenerateFullPaths=true", "/consoleloggerparameters:NoSummary" ], + "options": { + "cwd": "${workspaceFolder}" + }, "problemMatcher": "$msCompile" }, { @@ -115,6 +137,9 @@ "/property:GenerateFullPaths=true", "/consoleloggerparameters:NoSummary" ], + "options": { + "cwd": "${workspaceFolder}" + }, "problemMatcher": "$msCompile" }, { @@ -128,6 +153,9 @@ "/property:GenerateFullPaths=true", "/consoleloggerparameters:NoSummary" ], + "options": { + "cwd": "${workspaceFolder}" + }, "problemMatcher": "$msCompile" }, { @@ -141,6 +169,9 @@ "/property:GenerateFullPaths=true", "/consoleloggerparameters:NoSummary" ], + "options": { + "cwd": "${workspaceFolder}" + }, "problemMatcher": "$msCompile" }, { @@ -154,6 +185,9 @@ "/property:GenerateFullPaths=true", "/consoleloggerparameters:NoSummary" ], + "options": { + "cwd": "${workspaceFolder}" + }, "problemMatcher": "$msCompile" }, { @@ -167,6 +201,9 @@ "/property:GenerateFullPaths=true", "/consoleloggerparameters:NoSummary" ], + "options": { + "cwd": "${workspaceFolder}" + }, "problemMatcher": "$msCompile" }, { @@ -180,6 +217,29 @@ "/property:GenerateFullPaths=true", "/consoleloggerparameters:NoSummary" ], + "options": { + "cwd": "${workspaceFolder}" + }, + "problemMatcher": "$msCompile", + "group": { + "kind": "build", + "isDefault": true + } + }, + { + "label": "buildSeederAPI", + "hide": true, + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/util/SeederApi/SeederApi.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "options": { + "cwd": "${workspaceFolder}" + }, "problemMatcher": "$msCompile", "group": { "kind": "build", @@ -197,6 +257,9 @@ "/property:GenerateFullPaths=true", "/consoleloggerparameters:NoSummary" ], + "options": { + "cwd": "${workspaceFolder}" + }, "problemMatcher": "$msCompile", "group": { "kind": "build", @@ -214,6 +277,9 @@ "/property:GenerateFullPaths=true", "/consoleloggerparameters:NoSummary" ], + "options": { + "cwd": "${workspaceFolder}" + }, "problemMatcher": "$msCompile", "group": { "kind": "build", @@ -224,6 +290,9 @@ "label": "test", "type": "shell", "command": "dotnet test", + "options": { + "cwd": "${workspaceFolder}" + }, "group": { "kind": "test", "isDefault": true diff --git a/Directory.Build.props b/Directory.Build.props index db3ccf40f5..e7a8422605 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ net8.0 - 2025.12.2 + 2026.1.0 Bit.$(MSBuildProjectName) enable @@ -13,21 +13,21 @@ true - + - + 18.0.1 - + 2.6.6 - + 2.5.6 - + 6.0.0 - + 5.1.0 - + 4.18.1 - + 4.18.1 \ No newline at end of file diff --git a/bitwarden-server.sln b/bitwarden-server.sln index 055811478d..409906e2d0 100644 --- a/bitwarden-server.sln +++ b/bitwarden-server.sln @@ -137,10 +137,16 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RustSdk", "util\RustSdk\Rus EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharedWeb.Test", "test\SharedWeb.Test\SharedWeb.Test.csproj", "{AD59537D-5259-4B7A-948F-0CF58E80B359}" 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 @@ -353,6 +359,14 @@ Global {AD59537D-5259-4B7A-948F-0CF58E80B359}.Debug|Any CPU.Build.0 = Debug|Any CPU {AD59537D-5259-4B7A-948F-0CF58E80B359}.Release|Any CPU.ActiveCfg = Release|Any CPU {AD59537D-5259-4B7A-948F-0CF58E80B359}.Release|Any CPU.Build.0 = Release|Any CPU + {9F08DFBB-482B-4C9D-A5F4-6BDA6EC2E68F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9F08DFBB-482B-4C9D-A5F4-6BDA6EC2E68F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9F08DFBB-482B-4C9D-A5F4-6BDA6EC2E68F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9F08DFBB-482B-4C9D-A5F4-6BDA6EC2E68F}.Release|Any CPU.Build.0 = Release|Any CPU + {A2E067EF-609C-4D13-895A-E054C61D48BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A2E067EF-609C-4D13-895A-E054C61D48BB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A2E067EF-609C-4D13-895A-E054C61D48BB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A2E067EF-609C-4D13-895A-E054C61D48BB}.Release|Any CPU.Build.0 = Release|Any CPU {7D98784C-C253-43FB-9873-25B65C6250D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7D98784C-C253-43FB-9873-25B65C6250D6}.Debug|Any CPU.Build.0 = Debug|Any CPU {7D98784C-C253-43FB-9873-25B65C6250D6}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -361,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 @@ -417,8 +435,11 @@ Global {17A89266-260A-4A03-81AE-C0468C6EE06E} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E} {D1513D90-E4F5-44A9-9121-5E46E3E4A3F7} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E} {AD59537D-5259-4B7A-948F-0CF58E80B359} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F} + {9F08DFBB-482B-4C9D-A5F4-6BDA6EC2E68F} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E} + {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} diff --git a/bitwarden_license/src/Scim/Startup.cs b/bitwarden_license/src/Scim/Startup.cs index 2a84faa8dd..a912562f72 100644 --- a/bitwarden_license/src/Scim/Startup.cs +++ b/bitwarden_license/src/Scim/Startup.cs @@ -44,6 +44,7 @@ public class Startup // Repositories services.AddDatabaseRepositories(globalSettings); + services.AddTestPlayIdTracking(globalSettings); // Context services.AddScoped(); diff --git a/bitwarden_license/src/Sso/IdentityServer/DistributedCachePersistedGrantStore.cs b/bitwarden_license/src/Sso/IdentityServer/DistributedCachePersistedGrantStore.cs new file mode 100644 index 0000000000..ecb2f36cec --- /dev/null +++ b/bitwarden_license/src/Sso/IdentityServer/DistributedCachePersistedGrantStore.cs @@ -0,0 +1,102 @@ +using Bit.Sso.Utilities; +using Duende.IdentityServer.Models; +using Duende.IdentityServer.Stores; +using ZiggyCreatures.Caching.Fusion; + +namespace Bit.Sso.IdentityServer; + +/// +/// Distributed cache-backed persisted grant store for short-lived grants. +/// Uses IFusionCache (which wraps IDistributedCache) for horizontal scaling support, +/// and fall back to in-memory caching if Redis is not configured. +/// Designed for SSO authorization codes which are short-lived (5 minutes) and single-use. +/// +/// +/// This is purposefully a different implementation from how Identity solves Persisted Grants. +/// Because even flavored grant store, e.g., AuthorizationCodeGrantStore, can add intermediary +/// logic to a grant's handling by type, the fact that they all wrap IdentityServer's IPersistedGrantStore +/// leans on IdentityServer's opinion that all grants, regardless of type, go to the same persistence +/// mechanism (cache, database). +/// +/// +public class DistributedCachePersistedGrantStore : IPersistedGrantStore +{ + private readonly IFusionCache _cache; + + public DistributedCachePersistedGrantStore( + [FromKeyedServices(PersistedGrantsDistributedCacheConstants.CacheKey)] IFusionCache cache) + { + _cache = cache; + } + + public async Task GetAsync(string key) + { + var result = await _cache.TryGetAsync(key); + + if (!result.HasValue) + { + return null; + } + + var grant = result.Value; + + // Check if grant has expired - remove expired grants from cache + if (grant.Expiration.HasValue && grant.Expiration.Value < DateTime.UtcNow) + { + await RemoveAsync(key); + return null; + } + + return grant; + } + + public Task> GetAllAsync(PersistedGrantFilter filter) + { + // Cache stores are key-value based and don't support querying by filter criteria. + // This method is typically used for cleanup operations on long-lived grants in databases. + // For SSO's short-lived authorization codes, we rely on TTL expiration instead. + + return Task.FromResult(Enumerable.Empty()); + } + + public Task RemoveAllAsync(PersistedGrantFilter filter) + { + // Revocation Strategy: SSO's logout flow (AccountController.LogoutAsync) only clears local + // authentication cookies and performs federated logout with external IdPs. It does not invoke + // Duende's EndSession or TokenRevocation endpoints. Authorization codes are single-use and expire + // within 5 minutes, making explicit revocation unnecessary for SSO's security model. + // https://docs.duendesoftware.com/identityserver/reference/stores/persisted-grant-store/ + + // Cache stores are key-value based and don't support bulk deletion by filter. + // This method is typically used for cleanup operations on long-lived grants in databases. + // For SSO's short-lived authorization codes, we rely on TTL expiration instead. + + return Task.FromResult(0); + } + + public async Task RemoveAsync(string key) + { + await _cache.RemoveAsync(key); + } + + public async Task StoreAsync(PersistedGrant grant) + { + // Calculate TTL based on grant expiration + var duration = grant.Expiration.HasValue + ? grant.Expiration.Value - DateTime.UtcNow + : TimeSpan.FromMinutes(5); // Default to 5 minutes if no expiration set + + // Ensure positive duration + if (duration <= TimeSpan.Zero) + { + return; + } + + // Cache key "sso-grants:" is configured by service registration. Going through the consumed KeyedService will + // give us a consistent cache key prefix for these grants. + await _cache.SetAsync( + grant.Key, + grant, + new FusionCacheEntryOptions { Duration = duration }); + } +} diff --git a/bitwarden_license/src/Sso/Startup.cs b/bitwarden_license/src/Sso/Startup.cs index 2f83f3dad0..a2f363d533 100644 --- a/bitwarden_license/src/Sso/Startup.cs +++ b/bitwarden_license/src/Sso/Startup.cs @@ -41,6 +41,7 @@ public class Startup // Repositories services.AddDatabaseRepositories(globalSettings); + services.AddTestPlayIdTracking(globalSettings); // Context services.AddScoped(); diff --git a/bitwarden_license/src/Sso/Utilities/PersistedGrantsDistributedCacheConstants.cs b/bitwarden_license/src/Sso/Utilities/PersistedGrantsDistributedCacheConstants.cs new file mode 100644 index 0000000000..3ec45377e3 --- /dev/null +++ b/bitwarden_license/src/Sso/Utilities/PersistedGrantsDistributedCacheConstants.cs @@ -0,0 +1,10 @@ +namespace Bit.Sso.Utilities; + +public static class PersistedGrantsDistributedCacheConstants +{ + /// + /// The SSO Persisted Grant cache key. Identifies the keyed service consumed by the SSO Persisted Grant Store as + /// well as the cache key/namespace for grant storage. + /// + public const string CacheKey = "sso-grants"; +} diff --git a/bitwarden_license/src/Sso/Utilities/ServiceCollectionExtensions.cs b/bitwarden_license/src/Sso/Utilities/ServiceCollectionExtensions.cs index a51a04f5c8..da7a79535e 100644 --- a/bitwarden_license/src/Sso/Utilities/ServiceCollectionExtensions.cs +++ b/bitwarden_license/src/Sso/Utilities/ServiceCollectionExtensions.cs @@ -9,6 +9,7 @@ using Bit.Sso.IdentityServer; using Bit.Sso.Models; using Duende.IdentityServer.Models; using Duende.IdentityServer.ResponseHandling; +using Duende.IdentityServer.Stores; using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Sustainsys.Saml2.AspNetCore2; @@ -77,6 +78,17 @@ public static class ServiceCollectionExtensions }) .AddIdentityServerCertificate(env, globalSettings); + // PM-23572 + // Register named FusionCache for SSO authorization code grants. + // Provides separation of concerns and automatic Redis/in-memory negotiation + // .AddInMemoryCaching should still persist above; this handles configuration caching, etc., + // and is separate from this keyed service, which only serves grant negotiation. + services.AddExtendedCache(PersistedGrantsDistributedCacheConstants.CacheKey, globalSettings); + + // Store authorization codes in distributed cache for horizontal scaling + // Uses named FusionCache which gracefully degrades to in-memory when Redis isn't configured + services.AddSingleton(); + return identityServerBuilder; } } diff --git a/bitwarden_license/src/Sso/package-lock.json b/bitwarden_license/src/Sso/package-lock.json index f5e0468f87..efeee7f4ca 100644 --- a/bitwarden_license/src/Sso/package-lock.json +++ b/bitwarden_license/src/Sso/package-lock.json @@ -17,9 +17,9 @@ "css-loader": "7.1.2", "expose-loader": "5.0.1", "mini-css-extract-plugin": "2.9.2", - "sass": "1.93.2", + "sass": "1.97.2", "sass-loader": "16.0.5", - "webpack": "5.102.1", + "webpack": "5.104.1", "webpack-cli": "5.1.4" } }, @@ -749,9 +749,9 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.8.18", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.18.tgz", - "integrity": "sha512-UYmTpOBwgPScZpS4A+YbapwWuBwasxvO/2IOHArSsAhL/+ZdmATBXTex3t+l2hXwLVYK382ibr/nKoY9GKe86w==", + "version": "2.9.13", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.13.tgz", + "integrity": "sha512-WhtvB2NG2wjr04+h77sg3klAIwrgOqnjS49GGudnUPGFFgg7G17y7Qecqp+2Dr5kUDxNRBca0SK7cG8JwzkWDQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -792,9 +792,9 @@ } }, "node_modules/browserslist": { - "version": "4.26.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz", - "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "dev": true, "funding": [ { @@ -813,11 +813,11 @@ "license": "MIT", "peer": true, "dependencies": { - "baseline-browser-mapping": "^2.8.9", - "caniuse-lite": "^1.0.30001746", - "electron-to-chromium": "^1.5.227", - "node-releases": "^2.0.21", - "update-browserslist-db": "^1.1.3" + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" @@ -834,9 +834,9 @@ "license": "MIT" }, "node_modules/caniuse-lite": { - "version": "1.0.30001751", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz", - "integrity": "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==", + "version": "1.0.30001763", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001763.tgz", + "integrity": "sha512-mh/dGtq56uN98LlNX9qdbKnzINhX0QzhiWBFEkFfsFO4QyCvL8YegrJAazCwXIeqkIob8BlZPGM3xdnY+sgmvQ==", "dev": true, "funding": [ { @@ -988,9 +988,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.237", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.237.tgz", - "integrity": "sha512-icUt1NvfhGLar5lSWH3tHNzablaA5js3HVHacQimfP8ViEBOQv+L7DKEuHdbTZ0SKCO1ogTJTIL1Gwk9S6Qvcg==", + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", "dev": true, "license": "ISC" }, @@ -1022,9 +1022,9 @@ } }, "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", "dev": true, "license": "MIT" }, @@ -1418,13 +1418,17 @@ } }, "node_modules/loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", "dev": true, "license": "MIT", "engines": { "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/locate-path": { @@ -1541,9 +1545,9 @@ "optional": true }, "node_modules/node-releases": { - "version": "2.0.26", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.26.tgz", - "integrity": "sha512-S2M9YimhSjBSvYnlr5/+umAnPHE++ODwt5e2Ij6FoX45HA/s4vHdkDx1eax2pAPeAOqu4s9b7ppahsyEFdVqQA==", + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", "dev": true, "license": "MIT" }, @@ -1874,9 +1878,9 @@ "license": "MIT" }, "node_modules/sass": { - "version": "1.93.2", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.93.2.tgz", - "integrity": "sha512-t+YPtOQHpGW1QWsh1CHQ5cPIr9lbbGZLZnbihP/D/qZj/yuV68m8qarcV17nvkOX81BCrvzAlq2klCQFZghyTg==", + "version": "1.97.2", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.97.2.tgz", + "integrity": "sha512-y5LWb0IlbO4e97Zr7c3mlpabcbBtS+ieiZ9iwDooShpFKWXf62zz5pEPdwrLYm+Bxn1fnbwFGzHuCLSA9tBmrw==", "dev": true, "license": "MIT", "peer": true, @@ -2109,9 +2113,9 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.14", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", - "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", + "version": "5.3.16", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz", + "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==", "dev": true, "license": "MIT", "dependencies": { @@ -2165,9 +2169,9 @@ "license": "MIT" }, "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "dev": true, "funding": [ { @@ -2217,9 +2221,9 @@ } }, "node_modules/webpack": { - "version": "5.102.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.102.1.tgz", - "integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==", + "version": "5.104.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.104.1.tgz", + "integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==", "dev": true, "license": "MIT", "peer": true, @@ -2232,21 +2236,21 @@ "@webassemblyjs/wasm-parser": "^1.14.1", "acorn": "^8.15.0", "acorn-import-phases": "^1.0.3", - "browserslist": "^4.26.3", + "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.3", - "es-module-lexer": "^1.2.1", + "enhanced-resolve": "^5.17.4", + "es-module-lexer": "^2.0.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", + "loader-runner": "^4.3.1", "mime-types": "^2.1.27", "neo-async": "^2.6.2", "schema-utils": "^4.3.3", "tapable": "^2.3.0", - "terser-webpack-plugin": "^5.3.11", + "terser-webpack-plugin": "^5.3.16", "watchpack": "^2.4.4", "webpack-sources": "^3.3.3" }, diff --git a/bitwarden_license/src/Sso/package.json b/bitwarden_license/src/Sso/package.json index df46444aca..b0a1849421 100644 --- a/bitwarden_license/src/Sso/package.json +++ b/bitwarden_license/src/Sso/package.json @@ -16,9 +16,9 @@ "css-loader": "7.1.2", "expose-loader": "5.0.1", "mini-css-extract-plugin": "2.9.2", - "sass": "1.93.2", + "sass": "1.97.2", "sass-loader": "16.0.5", - "webpack": "5.102.1", + "webpack": "5.104.1", "webpack-cli": "5.1.4" } } diff --git a/bitwarden_license/test/SSO.Test/IdentityServer/DistributedCachePersistedGrantStoreTests.cs b/bitwarden_license/test/SSO.Test/IdentityServer/DistributedCachePersistedGrantStoreTests.cs new file mode 100644 index 0000000000..c0aa93f068 --- /dev/null +++ b/bitwarden_license/test/SSO.Test/IdentityServer/DistributedCachePersistedGrantStoreTests.cs @@ -0,0 +1,257 @@ +using Bit.Sso.IdentityServer; +using Duende.IdentityServer.Models; +using Duende.IdentityServer.Stores; +using NSubstitute; +using ZiggyCreatures.Caching.Fusion; + +namespace Bit.SSO.Test.IdentityServer; + +public class DistributedCachePersistedGrantStoreTests +{ + private readonly IFusionCache _cache; + private readonly DistributedCachePersistedGrantStore _sut; + + public DistributedCachePersistedGrantStoreTests() + { + _cache = Substitute.For(); + _sut = new DistributedCachePersistedGrantStore(_cache); + } + + [Fact] + public async Task StoreAsync_StoresGrantWithCalculatedTTL() + { + // Arrange + var grant = CreateTestGrant("test-key", expiration: DateTime.UtcNow.AddMinutes(5)); + + // Act + await _sut.StoreAsync(grant); + + // Assert + await _cache.Received(1).SetAsync( + "test-key", + grant, + Arg.Is(opts => + opts.Duration >= TimeSpan.FromMinutes(4.9) && + opts.Duration <= TimeSpan.FromMinutes(5))); + } + + [Fact] + public async Task StoreAsync_WithNoExpiration_UsesDefaultFiveMinuteTTL() + { + // Arrange + var grant = CreateTestGrant("no-expiry-key", expiration: null); + + // Act + await _sut.StoreAsync(grant); + + // Assert + await _cache.Received(1).SetAsync( + "no-expiry-key", + grant, + Arg.Is(opts => opts.Duration == TimeSpan.FromMinutes(5))); + } + + [Fact] + public async Task StoreAsync_WithAlreadyExpiredGrant_DoesNotStore() + { + // Arrange + var expiredGrant = CreateTestGrant("expired-key", expiration: DateTime.UtcNow.AddMinutes(-1)); + + // Act + await _sut.StoreAsync(expiredGrant); + + // Assert + await _cache.DidNotReceive().SetAsync( + Arg.Any(), + Arg.Any(), + Arg.Any()); + } + + [Fact] + public async Task StoreAsync_EnablesDistributedCache() + { + // Arrange + var grant = CreateTestGrant("distributed-key", expiration: DateTime.UtcNow.AddMinutes(5)); + + // Act + await _sut.StoreAsync(grant); + + // Assert + await _cache.Received(1).SetAsync( + "distributed-key", + grant, + Arg.Is(opts => + opts.SkipDistributedCache == false && + opts.SkipDistributedCacheReadWhenStale == false)); + } + + [Fact] + public async Task GetAsync_WithValidGrant_ReturnsGrant() + { + // Arrange + var grant = CreateTestGrant("valid-key", expiration: DateTime.UtcNow.AddMinutes(5)); + _cache.TryGetAsync("valid-key") + .Returns(MaybeValue.FromValue(grant)); + + // Act + var result = await _sut.GetAsync("valid-key"); + + // Assert + Assert.NotNull(result); + Assert.Equal("valid-key", result.Key); + Assert.Equal("authorization_code", result.Type); + Assert.Equal("test-subject", result.SubjectId); + await _cache.DidNotReceive().RemoveAsync(Arg.Any()); + } + + [Fact] + public async Task GetAsync_WithNonExistentKey_ReturnsNull() + { + // Arrange + _cache.TryGetAsync("nonexistent-key") + .Returns(MaybeValue.None); + + // Act + var result = await _sut.GetAsync("nonexistent-key"); + + // Assert + Assert.Null(result); + await _cache.DidNotReceive().RemoveAsync(Arg.Any()); + } + + [Fact] + public async Task GetAsync_WithExpiredGrant_RemovesAndReturnsNull() + { + // Arrange + var expiredGrant = CreateTestGrant("expired-key", expiration: DateTime.UtcNow.AddMinutes(-1)); + _cache.TryGetAsync("expired-key") + .Returns(MaybeValue.FromValue(expiredGrant)); + + // Act + var result = await _sut.GetAsync("expired-key"); + + // Assert + Assert.Null(result); + await _cache.Received(1).RemoveAsync("expired-key"); + } + + [Fact] + public async Task GetAsync_WithNoExpiration_ReturnsGrant() + { + // Arrange + var grant = CreateTestGrant("no-expiry-key", expiration: null); + _cache.TryGetAsync("no-expiry-key") + .Returns(MaybeValue.FromValue(grant)); + + // Act + var result = await _sut.GetAsync("no-expiry-key"); + + // Assert + Assert.NotNull(result); + Assert.Equal("no-expiry-key", result.Key); + Assert.Null(result.Expiration); + await _cache.DidNotReceive().RemoveAsync(Arg.Any()); + } + + [Fact] + public async Task RemoveAsync_RemovesGrantFromCache() + { + // Act + await _sut.RemoveAsync("remove-key"); + + // Assert + await _cache.Received(1).RemoveAsync("remove-key"); + } + + [Fact] + public async Task GetAllAsync_ReturnsEmptyCollection() + { + // Arrange + var filter = new PersistedGrantFilter + { + SubjectId = "test-subject", + SessionId = "test-session", + ClientId = "test-client", + Type = "authorization_code" + }; + + // Act + var result = await _sut.GetAllAsync(filter); + + // Assert + Assert.NotNull(result); + Assert.Empty(result); + } + + [Fact] + public async Task RemoveAllAsync_CompletesWithoutError() + { + // Arrange + var filter = new PersistedGrantFilter + { + SubjectId = "test-subject", + ClientId = "test-client" + }; + + // Act & Assert - should not throw + await _sut.RemoveAllAsync(filter); + + // Verify no cache operations were performed + await _cache.DidNotReceive().RemoveAsync(Arg.Any()); + } + + [Fact] + public async Task StoreAsync_PreservesAllGrantProperties() + { + // Arrange + var grant = new PersistedGrant + { + Key = "full-grant-key", + Type = "authorization_code", + SubjectId = "user-123", + SessionId = "session-456", + ClientId = "client-789", + Description = "Test grant", + CreationTime = DateTime.UtcNow.AddMinutes(-1), + Expiration = DateTime.UtcNow.AddMinutes(5), + ConsumedTime = null, + Data = "{\"test\":\"data\"}" + }; + + PersistedGrant? capturedGrant = null; + await _cache.SetAsync( + Arg.Any(), + Arg.Do(g => capturedGrant = g), + Arg.Any()); + + // Act + await _sut.StoreAsync(grant); + + // Assert + Assert.NotNull(capturedGrant); + Assert.Equal(grant.Key, capturedGrant.Key); + Assert.Equal(grant.Type, capturedGrant.Type); + Assert.Equal(grant.SubjectId, capturedGrant.SubjectId); + Assert.Equal(grant.SessionId, capturedGrant.SessionId); + Assert.Equal(grant.ClientId, capturedGrant.ClientId); + Assert.Equal(grant.Description, capturedGrant.Description); + Assert.Equal(grant.CreationTime, capturedGrant.CreationTime); + Assert.Equal(grant.Expiration, capturedGrant.Expiration); + Assert.Equal(grant.ConsumedTime, capturedGrant.ConsumedTime); + Assert.Equal(grant.Data, capturedGrant.Data); + } + + private static PersistedGrant CreateTestGrant(string key, DateTime? expiration) + { + return new PersistedGrant + { + Key = key, + Type = "authorization_code", + SubjectId = "test-subject", + ClientId = "test-client", + CreationTime = DateTime.UtcNow, + Expiration = expiration, + Data = "{\"test\":\"data\"}" + }; + } +} diff --git a/bitwarden_license/test/Scim.IntegrationTest/Scim.IntegrationTest.csproj b/bitwarden_license/test/Scim.IntegrationTest/Scim.IntegrationTest.csproj index 4fc79f2025..d0d329397c 100644 --- a/bitwarden_license/test/Scim.IntegrationTest/Scim.IntegrationTest.csproj +++ b/bitwarden_license/test/Scim.IntegrationTest/Scim.IntegrationTest.csproj @@ -1,35 +1,37 @@ - - - - false - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - - - - - true - PreserveNewest - Never - - - + + + + false + + $(WarningsNotAsErrors);CA1305 + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + true + PreserveNewest + Never + + + diff --git a/dev/setup_secrets.ps1 b/dev/setup_secrets.ps1 old mode 100644 new mode 100755 index 96dff04632..5013ca8bac --- a/dev/setup_secrets.ps1 +++ b/dev/setup_secrets.ps1 @@ -2,7 +2,7 @@ # Helper script for applying the same user secrets to each project param ( [switch]$clear, - [Parameter(ValueFromRemainingArguments = $true, Position=1)] + [Parameter(ValueFromRemainingArguments = $true, Position = 1)] $cmdArgs ) @@ -16,17 +16,18 @@ if ($clear -eq $true) { } $projects = @{ - Admin = "../src/Admin" - Api = "../src/Api" - Billing = "../src/Billing" - Events = "../src/Events" - EventsProcessor = "../src/EventsProcessor" - Icons = "../src/Icons" - Identity = "../src/Identity" - Notifications = "../src/Notifications" - Sso = "../bitwarden_license/src/Sso" - Scim = "../bitwarden_license/src/Scim" + Admin = "../src/Admin" + Api = "../src/Api" + Billing = "../src/Billing" + Events = "../src/Events" + EventsProcessor = "../src/EventsProcessor" + Icons = "../src/Icons" + Identity = "../src/Identity" + Notifications = "../src/Notifications" + Sso = "../bitwarden_license/src/Sso" + Scim = "../bitwarden_license/src/Scim" IntegrationTests = "../test/Infrastructure.IntegrationTest" + SeederApi = "../util/SeederApi" } foreach ($key in $projects.keys) { diff --git a/src/Admin/Admin.csproj b/src/Admin/Admin.csproj index cd30e841b4..b815ddea82 100644 --- a/src/Admin/Admin.csproj +++ b/src/Admin/Admin.csproj @@ -2,6 +2,8 @@ bitwarden-Admin + + $(WarningsNotAsErrors);CA1304;CA1305 diff --git a/src/Admin/HostedServices/DatabaseMigrationHostedService.cs b/src/Admin/HostedServices/DatabaseMigrationHostedService.cs index 219e6846bd..51739ce655 100644 --- a/src/Admin/HostedServices/DatabaseMigrationHostedService.cs +++ b/src/Admin/HostedServices/DatabaseMigrationHostedService.cs @@ -1,5 +1,5 @@ -using Bit.Core.Utilities; -using Microsoft.Data.SqlClient; +using System.Data.Common; +using Bit.Core.Utilities; namespace Bit.Admin.HostedServices; @@ -30,7 +30,7 @@ public class DatabaseMigrationHostedService : IHostedService, IDisposable // TODO: Maybe flip a flag somewhere to indicate migration is complete?? break; } - catch (SqlException e) + catch (DbException e) { if (i >= maxMigrationAttempts) { @@ -40,7 +40,7 @@ public class DatabaseMigrationHostedService : IHostedService, IDisposable else { _logger.LogError(e, - "Database unavailable for migration. Trying again (attempt #{0})...", i + 1); + "Database unavailable for migration. Trying again (attempt #{AttemptNumber})...", i + 1); await Task.Delay(20000, cancellationToken); } } diff --git a/src/Admin/Startup.cs b/src/Admin/Startup.cs index 87d68a7ac6..6c0a644ee6 100644 --- a/src/Admin/Startup.cs +++ b/src/Admin/Startup.cs @@ -65,6 +65,7 @@ public class Startup default: break; } + services.AddTestPlayIdTracking(globalSettings); // Context services.AddScoped(); diff --git a/src/Admin/package-lock.json b/src/Admin/package-lock.json index 6e0f78e1e6..e851daac36 100644 --- a/src/Admin/package-lock.json +++ b/src/Admin/package-lock.json @@ -18,9 +18,9 @@ "css-loader": "7.1.2", "expose-loader": "5.0.1", "mini-css-extract-plugin": "2.9.2", - "sass": "1.93.2", + "sass": "1.97.2", "sass-loader": "16.0.5", - "webpack": "5.102.1", + "webpack": "5.104.1", "webpack-cli": "5.1.4" } }, @@ -750,9 +750,9 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.8.18", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.18.tgz", - "integrity": "sha512-UYmTpOBwgPScZpS4A+YbapwWuBwasxvO/2IOHArSsAhL/+ZdmATBXTex3t+l2hXwLVYK382ibr/nKoY9GKe86w==", + "version": "2.9.13", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.13.tgz", + "integrity": "sha512-WhtvB2NG2wjr04+h77sg3klAIwrgOqnjS49GGudnUPGFFgg7G17y7Qecqp+2Dr5kUDxNRBca0SK7cG8JwzkWDQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -793,9 +793,9 @@ } }, "node_modules/browserslist": { - "version": "4.26.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz", - "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "dev": true, "funding": [ { @@ -814,11 +814,11 @@ "license": "MIT", "peer": true, "dependencies": { - "baseline-browser-mapping": "^2.8.9", - "caniuse-lite": "^1.0.30001746", - "electron-to-chromium": "^1.5.227", - "node-releases": "^2.0.21", - "update-browserslist-db": "^1.1.3" + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" @@ -835,9 +835,9 @@ "license": "MIT" }, "node_modules/caniuse-lite": { - "version": "1.0.30001751", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz", - "integrity": "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==", + "version": "1.0.30001763", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001763.tgz", + "integrity": "sha512-mh/dGtq56uN98LlNX9qdbKnzINhX0QzhiWBFEkFfsFO4QyCvL8YegrJAazCwXIeqkIob8BlZPGM3xdnY+sgmvQ==", "dev": true, "funding": [ { @@ -989,9 +989,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.237", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.237.tgz", - "integrity": "sha512-icUt1NvfhGLar5lSWH3tHNzablaA5js3HVHacQimfP8ViEBOQv+L7DKEuHdbTZ0SKCO1ogTJTIL1Gwk9S6Qvcg==", + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", "dev": true, "license": "ISC" }, @@ -1023,9 +1023,9 @@ } }, "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", "dev": true, "license": "MIT" }, @@ -1419,13 +1419,17 @@ } }, "node_modules/loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", "dev": true, "license": "MIT", "engines": { "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/locate-path": { @@ -1542,9 +1546,9 @@ "optional": true }, "node_modules/node-releases": { - "version": "2.0.26", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.26.tgz", - "integrity": "sha512-S2M9YimhSjBSvYnlr5/+umAnPHE++ODwt5e2Ij6FoX45HA/s4vHdkDx1eax2pAPeAOqu4s9b7ppahsyEFdVqQA==", + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", "dev": true, "license": "MIT" }, @@ -1875,9 +1879,9 @@ "license": "MIT" }, "node_modules/sass": { - "version": "1.93.2", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.93.2.tgz", - "integrity": "sha512-t+YPtOQHpGW1QWsh1CHQ5cPIr9lbbGZLZnbihP/D/qZj/yuV68m8qarcV17nvkOX81BCrvzAlq2klCQFZghyTg==", + "version": "1.97.2", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.97.2.tgz", + "integrity": "sha512-y5LWb0IlbO4e97Zr7c3mlpabcbBtS+ieiZ9iwDooShpFKWXf62zz5pEPdwrLYm+Bxn1fnbwFGzHuCLSA9tBmrw==", "dev": true, "license": "MIT", "peer": true, @@ -2110,9 +2114,9 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.14", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", - "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", + "version": "5.3.16", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz", + "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==", "dev": true, "license": "MIT", "dependencies": { @@ -2174,9 +2178,9 @@ "license": "MIT" }, "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "dev": true, "funding": [ { @@ -2226,9 +2230,9 @@ } }, "node_modules/webpack": { - "version": "5.102.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.102.1.tgz", - "integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==", + "version": "5.104.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.104.1.tgz", + "integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==", "dev": true, "license": "MIT", "peer": true, @@ -2241,21 +2245,21 @@ "@webassemblyjs/wasm-parser": "^1.14.1", "acorn": "^8.15.0", "acorn-import-phases": "^1.0.3", - "browserslist": "^4.26.3", + "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.3", - "es-module-lexer": "^1.2.1", + "enhanced-resolve": "^5.17.4", + "es-module-lexer": "^2.0.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", + "loader-runner": "^4.3.1", "mime-types": "^2.1.27", "neo-async": "^2.6.2", "schema-utils": "^4.3.3", "tapable": "^2.3.0", - "terser-webpack-plugin": "^5.3.11", + "terser-webpack-plugin": "^5.3.16", "watchpack": "^2.4.4", "webpack-sources": "^3.3.3" }, diff --git a/src/Admin/package.json b/src/Admin/package.json index f6f21e2cf9..3a3926d6ee 100644 --- a/src/Admin/package.json +++ b/src/Admin/package.json @@ -17,9 +17,9 @@ "css-loader": "7.1.2", "expose-loader": "5.0.1", "mini-css-extract-plugin": "2.9.2", - "sass": "1.93.2", + "sass": "1.97.2", "sass-loader": "16.0.5", - "webpack": "5.102.1", + "webpack": "5.104.1", "webpack-cli": "5.1.4" } } diff --git a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs index 5cdd857f3f..90d02a46a1 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs @@ -665,11 +665,6 @@ public class OrganizationUsersController : BaseAdminConsoleController [Authorize] public async Task> BulkRevokeAsync(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model) { - if (!_featureService.IsEnabled(FeatureFlagKeys.BulkRevokeUsersV2)) - { - return await RestoreOrRevokeUsersAsync(orgId, model, _revokeOrganizationUserCommand.RevokeUsersAsync); - } - var currentUserId = _userService.GetProperUserId(User); if (currentUserId == null) { diff --git a/src/Api/Api.csproj b/src/Api/Api.csproj index dd27de2e63..d25b989d11 100644 --- a/src/Api/Api.csproj +++ b/src/Api/Api.csproj @@ -4,6 +4,8 @@ false bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml true + + $(WarningsNotAsErrors);CA1304;CA1305 diff --git a/src/Api/Auth/Models/Request/EmergencyAccessRequestModels.cs b/src/Api/Auth/Models/Request/EmergencyAccessRequestModels.cs index 33a7e52791..75e96ebc66 100644 --- a/src/Api/Auth/Models/Request/EmergencyAccessRequestModels.cs +++ b/src/Api/Auth/Models/Request/EmergencyAccessRequestModels.cs @@ -36,7 +36,7 @@ public class EmergencyAccessUpdateRequestModel existingEmergencyAccess.KeyEncrypted = KeyEncrypted; } existingEmergencyAccess.Type = Type; - existingEmergencyAccess.WaitTimeDays = WaitTimeDays; + existingEmergencyAccess.WaitTimeDays = (short)WaitTimeDays; return existingEmergencyAccess; } } diff --git a/src/Api/Startup.cs b/src/Api/Startup.cs index 2f16470cd4..b201cef0f3 100644 --- a/src/Api/Startup.cs +++ b/src/Api/Startup.cs @@ -85,6 +85,7 @@ public class Startup // Repositories services.AddDatabaseRepositories(globalSettings); + services.AddTestPlayIdTracking(globalSettings); // Context services.AddScoped(); diff --git a/src/Api/Tools/Controllers/SendsController.cs b/src/Api/Tools/Controllers/SendsController.cs index 449d9573fd..f9f71d076d 100644 --- a/src/Api/Tools/Controllers/SendsController.cs +++ b/src/Api/Tools/Controllers/SendsController.cs @@ -5,9 +5,11 @@ using Bit.Api.Tools.Models.Request; using Bit.Api.Tools.Models.Response; using Bit.Api.Utilities; using Bit.Core; +using Bit.Core.Auth.Identity; +using Bit.Core.Auth.UserFeatures.SendAccess; using Bit.Core.Exceptions; +using Bit.Core.Platform.Push; using Bit.Core.Services; -using Bit.Core.Settings; using Bit.Core.Tools.Enums; using Bit.Core.Tools.Models.Data; using Bit.Core.Tools.Repositories; @@ -22,7 +24,6 @@ using Microsoft.AspNetCore.Mvc; namespace Bit.Api.Tools.Controllers; [Route("sends")] -[Authorize("Application")] public class SendsController : Controller { private readonly ISendRepository _sendRepository; @@ -31,11 +32,10 @@ public class SendsController : Controller private readonly ISendFileStorageService _sendFileStorageService; private readonly IAnonymousSendCommand _anonymousSendCommand; private readonly INonAnonymousSendCommand _nonAnonymousSendCommand; - private readonly ISendOwnerQuery _sendOwnerQuery; - private readonly ILogger _logger; - private readonly GlobalSettings _globalSettings; + private readonly IFeatureService _featureService; + private readonly IPushNotificationService _pushNotificationService; public SendsController( ISendRepository sendRepository, @@ -46,7 +46,8 @@ public class SendsController : Controller ISendOwnerQuery sendOwnerQuery, ISendFileStorageService sendFileStorageService, ILogger logger, - GlobalSettings globalSettings) + IFeatureService featureService, + IPushNotificationService pushNotificationService) { _sendRepository = sendRepository; _userService = userService; @@ -56,10 +57,12 @@ public class SendsController : Controller _sendOwnerQuery = sendOwnerQuery; _sendFileStorageService = sendFileStorageService; _logger = logger; - _globalSettings = globalSettings; + _featureService = featureService; + _pushNotificationService = pushNotificationService; } #region Anonymous endpoints + [AllowAnonymous] [HttpPost("access/{id}")] public async Task Access(string id, [FromBody] SendAccessRequestModel model) @@ -73,21 +76,32 @@ public class SendsController : Controller var guid = new Guid(CoreHelpers.Base64UrlDecode(id)); var send = await _sendRepository.GetByIdAsync(guid); + if (send == null) { throw new BadRequestException("Could not locate send"); } + + /* This guard can be removed once feature flag is retired*/ + var sendEmailOtpEnabled = _featureService.IsEnabled(FeatureFlagKeys.SendEmailOTP); + if (sendEmailOtpEnabled && send.AuthType == AuthType.Email && send.Emails is not null) + { + return new UnauthorizedResult(); + } + var sendAuthResult = await _sendAuthorizationService.AccessAsync(send, model.Password); if (sendAuthResult.Equals(SendAccessResult.PasswordRequired)) { return new UnauthorizedResult(); } + if (sendAuthResult.Equals(SendAccessResult.PasswordInvalid)) { await Task.Delay(2000); throw new BadRequestException("Invalid password."); } + if (sendAuthResult.Equals(SendAccessResult.Denied)) { throw new NotFoundException(); @@ -99,6 +113,7 @@ public class SendsController : Controller var creator = await _userService.GetUserByIdAsync(send.UserId.Value); sendResponse.CreatorIdentifier = creator.Email; } + return new ObjectResult(sendResponse); } @@ -122,6 +137,13 @@ public class SendsController : Controller throw new BadRequestException("Could not locate send"); } + /* This guard can be removed once feature flag is retired*/ + var sendEmailOtpEnabled = _featureService.IsEnabled(FeatureFlagKeys.SendEmailOTP); + if (sendEmailOtpEnabled && send.AuthType == AuthType.Email && send.Emails is not null) + { + return new UnauthorizedResult(); + } + var (url, result) = await _anonymousSendCommand.GetSendFileDownloadUrlAsync(send, fileId, model.Password); @@ -129,21 +151,19 @@ public class SendsController : Controller { return new UnauthorizedResult(); } + if (result.Equals(SendAccessResult.PasswordInvalid)) { await Task.Delay(2000); throw new BadRequestException("Invalid password."); } + if (result.Equals(SendAccessResult.Denied)) { throw new NotFoundException(); } - return new ObjectResult(new SendFileDownloadDataResponseModel() - { - Id = fileId, - Url = url, - }); + return new ObjectResult(new SendFileDownloadDataResponseModel() { Id = fileId, Url = url, }); } [AllowAnonymous] @@ -157,7 +177,8 @@ public class SendsController : Controller { try { - var blobName = eventGridEvent.Subject.Split($"{AzureSendFileStorageService.FilesContainerName}/blobs/")[1]; + var blobName = + eventGridEvent.Subject.Split($"{AzureSendFileStorageService.FilesContainerName}/blobs/")[1]; var sendId = AzureSendFileStorageService.SendIdFromBlobName(blobName); var send = await _sendRepository.GetByIdAsync(new Guid(sendId)); if (send == null) @@ -166,6 +187,7 @@ public class SendsController : Controller { await azureSendFileStorageService.DeleteBlobAsync(blobName); } + return; } @@ -173,7 +195,8 @@ public class SendsController : Controller } catch (Exception e) { - _logger.LogError(e, "Uncaught exception occurred while handling event grid event: {Event}", JsonSerializer.Serialize(eventGridEvent)); + _logger.LogError(e, "Uncaught exception occurred while handling event grid event: {Event}", + JsonSerializer.Serialize(eventGridEvent)); return; } } @@ -185,6 +208,7 @@ public class SendsController : Controller #region Non-anonymous endpoints + [Authorize(Policies.Application)] [HttpGet("{id}")] public async Task Get(string id) { @@ -193,6 +217,7 @@ public class SendsController : Controller return new SendResponseModel(send); } + [Authorize(Policies.Application)] [HttpGet("")] public async Task> GetAll() { @@ -203,6 +228,67 @@ public class SendsController : Controller return result; } + [Authorize(Policy = Policies.Send)] + // [RequireFeature(FeatureFlagKeys.SendEmailOTP)] /* Uncomment once client fallback re-try logic is added */ + [HttpPost("access/")] + public async Task AccessUsingAuth() + { + var guid = User.GetSendId(); + var send = await _sendRepository.GetByIdAsync(guid); + if (send == null) + { + throw new BadRequestException("Could not locate send"); + } + if (send.MaxAccessCount.GetValueOrDefault(int.MaxValue) <= send.AccessCount || + send.ExpirationDate.GetValueOrDefault(DateTime.MaxValue) < DateTime.UtcNow || send.Disabled || + send.DeletionDate < DateTime.UtcNow) + { + throw new NotFoundException(); + } + + var sendResponse = new SendAccessResponseModel(send); + if (send.UserId.HasValue && !send.HideEmail.GetValueOrDefault()) + { + var creator = await _userService.GetUserByIdAsync(send.UserId.Value); + sendResponse.CreatorIdentifier = creator.Email; + } + + send.AccessCount++; + await _sendRepository.ReplaceAsync(send); + await _pushNotificationService.PushSyncSendUpdateAsync(send); + + return new ObjectResult(sendResponse); + } + + [Authorize(Policy = Policies.Send)] + // [RequireFeature(FeatureFlagKeys.SendEmailOTP)] /* Uncomment once client fallback re-try logic is added */ + [HttpPost("access/file/{fileId}")] + public async Task GetSendFileDownloadDataUsingAuth(string fileId) + { + var sendId = User.GetSendId(); + var send = await _sendRepository.GetByIdAsync(sendId); + + if (send == null) + { + throw new BadRequestException("Could not locate send"); + } + if (send.MaxAccessCount.GetValueOrDefault(int.MaxValue) <= send.AccessCount || + send.ExpirationDate.GetValueOrDefault(DateTime.MaxValue) < DateTime.UtcNow || send.Disabled || + send.DeletionDate < DateTime.UtcNow) + { + throw new NotFoundException(); + } + + var url = await _sendFileStorageService.GetSendFileDownloadUrlAsync(send, fileId); + + send.AccessCount++; + await _sendRepository.ReplaceAsync(send); + await _pushNotificationService.PushSyncSendUpdateAsync(send); + + return new ObjectResult(new SendFileDownloadDataResponseModel() { Id = fileId, Url = url }); + } + + [Authorize(Policies.Application)] [HttpPost("")] public async Task Post([FromBody] SendRequestModel model) { @@ -213,6 +299,7 @@ public class SendsController : Controller return new SendResponseModel(send); } + [Authorize(Policies.Application)] [HttpPost("file/v2")] public async Task PostFile([FromBody] SendRequestModel model) { @@ -243,6 +330,7 @@ public class SendsController : Controller }; } + [Authorize(Policies.Application)] [HttpGet("{id}/file/{fileId}")] public async Task RenewFileUpload(string id, string fileId) { @@ -267,6 +355,7 @@ public class SendsController : Controller }; } + [Authorize(Policies.Application)] [HttpPost("{id}/file/{fileId}")] [SelfHosted(SelfHostedOnly = true)] [RequestSizeLimit(Constants.FileSize501mb)] @@ -283,12 +372,14 @@ public class SendsController : Controller { throw new BadRequestException("Could not locate send"); } + await Request.GetFileAsync(async (stream) => { await _nonAnonymousSendCommand.UploadFileToExistingSendAsync(stream, send); }); } + [Authorize(Policies.Application)] [HttpPut("{id}")] public async Task Put(string id, [FromBody] SendRequestModel model) { @@ -304,6 +395,7 @@ public class SendsController : Controller return new SendResponseModel(send); } + [Authorize(Policies.Application)] [HttpPut("{id}/remove-password")] public async Task PutRemovePassword(string id) { @@ -322,6 +414,28 @@ public class SendsController : Controller return new SendResponseModel(send); } + // Removes ALL authentication (email or password) if any is present + [Authorize(Policies.Application)] + [HttpPut("{id}/remove-auth")] + public async Task PutRemoveAuth(string id) + { + var userId = _userService.GetProperUserId(User) ?? throw new InvalidOperationException("User ID not found"); + var send = await _sendRepository.GetByIdAsync(new Guid(id)); + if (send == null || send.UserId != userId) + { + throw new NotFoundException(); + } + + // This endpoint exists because PUT preserves existing Password/Emails when not provided. + // This allows clients to update other fields without re-submitting sensitive auth data. + send.Password = null; + send.Emails = null; + send.AuthType = AuthType.None; + await _nonAnonymousSendCommand.SaveSendAsync(send); + return new SendResponseModel(send); + } + + [Authorize(Policies.Application)] [HttpDelete("{id}")] public async Task Delete(string id) { diff --git a/src/Billing/Billing.csproj b/src/Billing/Billing.csproj index c7620d6df8..27ee9a7ce3 100644 --- a/src/Billing/Billing.csproj +++ b/src/Billing/Billing.csproj @@ -3,6 +3,8 @@ bitwarden-Billing + + $(WarningsNotAsErrors);CA1305 diff --git a/src/Billing/Startup.cs b/src/Billing/Startup.cs index 30f4f5f562..f5f98bfd53 100644 --- a/src/Billing/Startup.cs +++ b/src/Billing/Startup.cs @@ -48,6 +48,7 @@ public class Startup // Repositories services.AddDatabaseRepositories(globalSettings); + services.AddTestPlayIdTracking(globalSettings); // PayPal IPN Client services.AddHttpClient(); diff --git a/src/Core/AdminConsole/Models/Data/Organizations/SelfHostedOrganizationDetails.cs b/src/Core/AdminConsole/Models/Data/Organizations/SelfHostedOrganizationDetails.cs index d74fb4f138..5ec9dc255a 100644 --- a/src/Core/AdminConsole/Models/Data/Organizations/SelfHostedOrganizationDetails.cs +++ b/src/Core/AdminConsole/Models/Data/Organizations/SelfHostedOrganizationDetails.cs @@ -128,6 +128,7 @@ public class SelfHostedOrganizationDetails : Organization UseApi = UseApi, UseResetPassword = UseResetPassword, UseSecretsManager = UseSecretsManager, + UsePasswordManager = UsePasswordManager, SelfHost = SelfHost, UsersGetPremium = UsersGetPremium, UseCustomPermissions = UseCustomPermissions, @@ -156,6 +157,8 @@ public class SelfHostedOrganizationDetails : Organization UseAdminSponsoredFamilies = UseAdminSponsoredFamilies, UseDisableSmAdsForUsers = UseDisableSmAdsForUsers, UsePhishingBlocker = UsePhishingBlocker, + UseOrganizationDomains = UseOrganizationDomains, + UseAutomaticUserConfirmation = UseAutomaticUserConfirmation, }; } } diff --git a/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationConfirmation/OrganizationConfirmationEnterpriseTeamsView.html.hbs b/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationConfirmation/OrganizationConfirmationEnterpriseTeamsView.html.hbs index 8477efff26..3c8f498403 100644 --- a/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationConfirmation/OrganizationConfirmationEnterpriseTeamsView.html.hbs +++ b/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationConfirmation/OrganizationConfirmationEnterpriseTeamsView.html.hbs @@ -1,6 +1,6 @@ - + @@ -8,808 +8,807 @@ - - - - - - - + + + + + + + - - - - + + + + - + - - - - - -
- - - - - -
- + + + + + +
+ + + + + +
+ - + - - - -
- - - - - - - - -
- - - - - -
- - - - - - +
- - -
- - - - - - - - - - - - - - - - - -
- - - - - - - -
- - - -
- +
+ + + + + + + + +
+ + + + + +
+ + + + + + + +
+ + +
+ + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ + + +
+ +
+ +

+ You can now share passwords with members of {{OrganizationName}}! +

+ +
+ + + + + + + +
+ + Log in + +
+ +
+ +
+ + + +
+ + + + + + + + + +
+ + + + + + + +
+ + + +
+ +
+ +
+ + +
+ +
+ + + + + +
+ +
- -

- You can now share passwords with members of {{OrganizationName}}! -

- -
- - - - - - - -
- - Log in - -
- -
- -
- - - -
- - - - - - - - - -
- - - - - - - -
- - - -
- -
- -
- - -
- -
- - - - - -
- - -
- -
- - - - + +
+ + + + - - - - -
- + + + + +
+ - + - - - -
- - - -
- - - - - - - -
- - -
- - - - - - - - - -
- -
As a member of {{OrganizationName}}:
- -
- -
- - -
- -
- - - - - -
- - - - - - - -
- - -
- - -
- - - - - - - - - -
- - - - - - - -
- - Organization Icon - -
- -
- -
- - - -
- - - - - - - - - -
- -
Your account is owned by {{OrganizationName}} and is subject to their security and management policies.
- -
- -
- - -
- - -
- -
- - - - - -
- - - - - - +
- - -
- - -
- - - - - - - - - -
- - - - - - - -
- - Group Users Icon - -
- -
- -
- - - -
- - - - - - - - - - - - - -
- -
You can easily access and share passwords with your team.
- -
- - + + + +
+ + + + + + + +
+ + +
+ + + + + + + + + +
+ +
As a member of {{OrganizationName}}:
+ +
+ +
+ + +
+ +
+ + + + + +
+ + + + + + + +
+ + +
+ + +
+ + + + + + + + + +
+ + + + + + + +
+ + Organization Icon + +
+ +
+ +
+ + + +
+ + + + + + + + + +
+ +
Your account is owned by {{OrganizationName}} and is subject to their security and management policies.
+ +
+ +
+ + +
+ + +
+ +
+ + + + + +
+ + + + + + + +
+ + +
+ + +
+ + + + + + + + + +
+ + + + + + + +
+ + Group Users Icon + +
+ +
+ +
+ + + +
+ + + + + + + + + + + + + +
+ +
You can easily access and share passwords with your team.
+ +
+ + - + + +
+ +
+ + +
+ + +
+ +
+ + + + + +
+ + + + + + + +
+ +
+ +
+ + +
- -
- - -
- - -
- -
- - - - - -
- - - - - - - -
- -
- -
- - - -
- -
- - - - + +
+ + + + - - - - -
- + + + + +
+ - + - - - -
- - - -
- - - - - - +
- - -
- - - - - - - - - -
- -

- Learn more about Bitwarden -

- Find user guides, product documentation, and videos on the - Bitwarden Help Center.
- +
+ + + +
+ + + + + + + +
+ + +
+ + + + + + + + + +
+ +

+ Learn more about Bitwarden +

+ Find user guides, product documentation, and videos on the + Bitwarden Help Center.
+ +
+ +
+ + + +
+ + + + + + + + + +
+ +
+ + +
+ +
+ + +
- -
- - - -
- - - - - - - - - -
- -
- - -
- -
- - - -
- -
- - - - + +
+ + + + - - - - -
- + + + + +
+ - + - - +
- - -
- - - - - - - - - - - - - -
- - - - - - - - - - - - -
- - - - - - -
- - - + + + +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + +
+ +

+ © 2025 Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102, Santa + Barbara, CA, USA +

+

+ Always confirm you are on a trusted Bitwarden domain before logging + in:
+ bitwarden.com | + Learn why we include this +

+ +
+ +
+ +
-
- - - - - - - - - - -
- - - - - - -
- - - -
-
- - - - - - - - - - -
- - - - - - -
- - - -
-
- - - - - - - - - - -
- - - - - - -
- - - -
-
- - - - - - - - - - -
- - - - - - -
- - - -
-
- - - - - - - - - - -
- - - - - - -
- - - -
-
- - - - - - - - - - -
- - - - - - -
- - - -
-
- - - -
- -

- © 2025 Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102, Santa - Barbara, CA, USA -

-

- Always confirm you are on a trusted Bitwarden domain before logging - in:
- bitwarden.com | - Learn why we include this -

- -
- -
- - -
- -
- - - - - -
- - + +
+ + + + + +
+ + - \ No newline at end of file diff --git a/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationConfirmation/OrganizationConfirmationFamilyFreeView.html.hbs b/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationConfirmation/OrganizationConfirmationFamilyFreeView.html.hbs index cbe09d3e93..c0f838e0c7 100644 --- a/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationConfirmation/OrganizationConfirmationFamilyFreeView.html.hbs +++ b/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationConfirmation/OrganizationConfirmationFamilyFreeView.html.hbs @@ -1,6 +1,6 @@ - + @@ -8,976 +8,976 @@ - - - - - - - + + + + + + + - - - - + + + + - + - - - - - -
- - - - - -
- + + + + + +
+ + + + + +
+ - + - - - -
- - - - - - - - -
- - - - - -
- - - - - - +
- - -
- - - - - - - - - - - - - - - - - -
- - - - - - - -
- - - -
- +
+ + + + + + + + +
+ + + + + +
+ + + + + + + +
+ + +
+ + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ + + +
+ +
+ +

+ You can now share passwords with members of {{OrganizationName}}! +

+ +
+ + + + + + + +
+ + Log in + +
+ +
+ +
+ + + +
+ + + + + + + + + +
+ + + + + + + +
+ + + +
+ +
+ +
+ + +
+ +
+ + + + + +
+ +
- -

- You can now share passwords with members of {{OrganizationName}}! -

- -
- - - - - - - -
- - Log in - -
- -
- -
- - - -
- - - - - - - - - -
- - - - - - - -
- - - -
- -
- -
- - -
- -
- - - - - -
- - -
- -
- - - - + +
+ + + + - - - - -
- + + + + +
+ - + - - - -
- - - -
- - - - - - - -
- - -
- - - - - - - - - -
- -
As a member of {{OrganizationName}}:
- -
- -
- - -
- -
- - - - - -
- - - - - - - -
- - -
- - -
- - - - - - - - - -
- - - - - - - -
- - Collections Icon - -
- -
- -
- - - -
- - - - - - - - - -
- -
You can access passwords {{OrganizationName}} has shared with you.
- -
- -
- - -
- - -
- -
- - - - - -
- - - - - - +
- - -
- - -
- - - - - - - - - -
- - - - - - - -
- - Group Users Icon - -
- -
- -
- - - -
- - - - - - - - - - - - - -
- -
You can easily share passwords with friends, family, or coworkers.
- -
- - + + + +
+ + + + + + + +
+ + +
+ + + + + + + + + +
+ +
As a member of {{OrganizationName}}:
+ +
+ +
+ + +
+ +
+ + + + + +
+ + + + + + + +
+ + +
+ + +
+ + + + + + + + + +
+ + + + + + + +
+ + Collections Icon + +
+ +
+ +
+ + + +
+ + + + + + + + + +
+ +
You can access passwords {{OrganizationName}} has shared with you.
+ +
+ +
+ + +
+ + +
+ +
+ + + + + +
+ + + + + + + +
+ + +
+ + +
+ + + + + + + + + +
+ + + + + + + +
+ + Group Users Icon + +
+ +
+ +
+ + + +
+ + + + + + + + + + + + + +
+ +
You can easily share passwords with friends, family, or coworkers.
+ +
+ + - + + +
+ +
+ + +
+ + +
+ +
+ + + + + +
+ + + + + + + +
+ +
+ +
+ + +
- -
- - -
- - -
- -
- - - - - -
- - - - - - - -
- -
- -
- - - -
- -
- - - - + +
+ + + + - - - - -
- + + + + +
+ - + - - - -
- - - -
- - - - - - +
- - -
- - - - - - - - - - - - - -
- -
Download Bitwarden on all devices
- +
+ + + +
+ + + + + + + +
+ + +
+ + + + + + + + + + + + + +
+ +
Download Bitwarden on all devices
+ +
+ +
Already using the browser extension? + Download the Bitwarden mobile app from the + App Store + or Google Play + to quickly save logins and autofill forms on the go.
+ +
+ +
+ + +
+ +
+ + + + + +
+ + + + + + + +
+ + +
+ + +
+ + + + + + + + + +
+ + + + + + + +
+ + + + Download on the App Store + + + +
+ +
+ +
+ + + +
+ + + + + + + + + +
+ + + + + + + +
+ + + + Get it on Google Play + + + +
+ +
+ +
+ + +
+ + +
+ +
+ + +
- -
Already using the browser extension? - Download the Bitwarden mobile app from the - App Store - or Google Play - to quickly save logins and autofill forms on the go.
- -
- -
- - -
- -
- - - - - -
- - - - - - - -
- - -
- - -
- - - - - - - - - -
- - - - - - - -
- - - - Download on the App Store - - - -
- -
- -
- - - -
- - - - - - - - - -
- - - - - - - -
- - - - Get it on Google Play - - - -
- -
- -
- - -
- - -
- -
- - - -
- -
- - - - + +
+ + + + - - - - -
- + + + + +
+ - + - - - -
- - - -
- - - - - - +
- - -
- - - - - - - - - -
- -

- Learn more about Bitwarden -

- Find user guides, product documentation, and videos on the - Bitwarden Help Center.
- +
+ + + +
+ + + + + + + +
+ + +
+ + + + + + + + + +
+ +

+ Learn more about Bitwarden +

+ Find user guides, product documentation, and videos on the + Bitwarden Help Center.
+ +
+ +
+ + + +
+ + + + + + + + + +
+ +
+ + +
+ +
+ + +
- -
- - - -
- - - - - - - - - -
- -
- - -
- -
- - - -
- -
- - - - + +
+ + + + - - - - -
- + + + + +
+ - + - - +
- - -
- - - - - - - - - - - - - -
- - - - - - - - - - - - -
- - - - - - -
- - - + + + +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + +
+ +

+ © 2025 Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102, Santa + Barbara, CA, USA +

+

+ Always confirm you are on a trusted Bitwarden domain before logging + in:
+ bitwarden.com | + Learn why we include this +

+ +
+ +
+ +
-
- - - - - - - - - - -
- - - - - - -
- - - -
-
- - - - - - - - - - -
- - - - - - -
- - - -
-
- - - - - - - - - - -
- - - - - - -
- - - -
-
- - - - - - - - - - -
- - - - - - -
- - - -
-
- - - - - - - - - - -
- - - - - - -
- - - -
-
- - - - - - - - - - -
- - - - - - -
- - - -
-
- - - -
- -

- © 2025 Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102, Santa - Barbara, CA, USA -

-

- Always confirm you are on a trusted Bitwarden domain before logging - in:
- bitwarden.com | - Learn why we include this -

- -
- -
- - -
- -
- - - - - -
- - + +
+ + + + + +
+ + - \ No newline at end of file + diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeUser/v1/IRevokeOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeUser/v1/IRevokeOrganizationUserCommand.cs index 7b5541c3ce..313c01af7c 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeUser/v1/IRevokeOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeUser/v1/IRevokeOrganizationUserCommand.cs @@ -7,6 +7,4 @@ public interface IRevokeOrganizationUserCommand { Task RevokeUserAsync(OrganizationUser organizationUser, Guid? revokingUserId); Task RevokeUserAsync(OrganizationUser organizationUser, EventSystemUser systemUser); - Task>> RevokeUsersAsync(Guid organizationId, - IEnumerable organizationUserIds, Guid? revokingUserId); } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeUser/v1/RevokeOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeUser/v1/RevokeOrganizationUserCommand.cs index 7aa67f0813..750ebf2518 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeUser/v1/RevokeOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeUser/v1/RevokeOrganizationUserCommand.cs @@ -68,68 +68,4 @@ public class RevokeOrganizationUserCommand( await organizationUserRepository.RevokeAsync(organizationUser.Id); organizationUser.Status = OrganizationUserStatusType.Revoked; } - - public async Task>> RevokeUsersAsync(Guid organizationId, - IEnumerable organizationUserIds, Guid? revokingUserId) - { - var orgUsers = await organizationUserRepository.GetManyAsync(organizationUserIds); - var filteredUsers = orgUsers.Where(u => u.OrganizationId == organizationId) - .ToList(); - - if (!filteredUsers.Any()) - { - throw new BadRequestException("Users invalid."); - } - - if (!await hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(organizationId, organizationUserIds)) - { - throw new BadRequestException("Organization must have at least one confirmed owner."); - } - - var deletingUserIsOwner = false; - if (revokingUserId.HasValue) - { - deletingUserIsOwner = await currentContext.OrganizationOwner(organizationId); - } - - var result = new List>(); - - foreach (var organizationUser in filteredUsers) - { - try - { - if (organizationUser.Status == OrganizationUserStatusType.Revoked) - { - throw new BadRequestException("Already revoked."); - } - - if (revokingUserId.HasValue && organizationUser.UserId == revokingUserId) - { - throw new BadRequestException("You cannot revoke yourself."); - } - - if (organizationUser.Type == OrganizationUserType.Owner && revokingUserId.HasValue && - !deletingUserIsOwner) - { - throw new BadRequestException("Only owners can revoke other owners."); - } - - await organizationUserRepository.RevokeAsync(organizationUser.Id); - organizationUser.Status = OrganizationUserStatusType.Revoked; - await eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Revoked); - if (organizationUser.UserId.HasValue) - { - await pushNotificationService.PushSyncOrgKeysAsync(organizationUser.UserId.Value); - } - - result.Add(Tuple.Create(organizationUser, "")); - } - catch (BadRequestException e) - { - result.Add(Tuple.Create(organizationUser, e.Message)); - } - } - - return result; - } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/AutomaticUserConfirmationPolicyEventHandler.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/AutomaticUserConfirmationPolicyEventHandler.cs index 86c94147f4..213d18c27d 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/AutomaticUserConfirmationPolicyEventHandler.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/AutomaticUserConfirmationPolicyEventHandler.cs @@ -74,8 +74,12 @@ public class AutomaticUserConfirmationPolicyEventHandler( private async Task ValidateUserComplianceWithSingleOrgAsync(Guid organizationId, ICollection organizationUsers) { - var hasNonCompliantUser = (await organizationUserRepository.GetManyByManyUsersAsync( - organizationUsers.Select(ou => ou.UserId!.Value))) + var userIds = organizationUsers.Where( + u => u.UserId is not null && + u.Status != OrganizationUserStatusType.Invited) + .Select(u => u.UserId!.Value); + + var hasNonCompliantUser = (await organizationUserRepository.GetManyByManyUsersAsync(userIds)) .Any(uo => uo.OrganizationId != organizationId && uo.Status != OrganizationUserStatusType.Invited); diff --git a/src/Core/Auth/Entities/EmergencyAccess.cs b/src/Core/Auth/Entities/EmergencyAccess.cs index d855126468..36aaf46a8c 100644 --- a/src/Core/Auth/Entities/EmergencyAccess.cs +++ b/src/Core/Auth/Entities/EmergencyAccess.cs @@ -18,7 +18,7 @@ public class EmergencyAccess : ITableObject public string KeyEncrypted { get; set; } public EmergencyAccessType Type { get; set; } public EmergencyAccessStatusType Status { get; set; } - public int WaitTimeDays { get; set; } + public short WaitTimeDays { get; set; } public DateTime? RecoveryInitiatedDate { get; set; } public DateTime? LastNotificationDate { get; set; } public DateTime CreationDate { get; set; } = DateTime.UtcNow; diff --git a/src/Core/Auth/Services/EmergencyAccess/EmergencyAccessService.cs b/src/Core/Auth/Services/EmergencyAccess/EmergencyAccessService.cs index 4331179554..0072f85e61 100644 --- a/src/Core/Auth/Services/EmergencyAccess/EmergencyAccessService.cs +++ b/src/Core/Auth/Services/EmergencyAccess/EmergencyAccessService.cs @@ -79,7 +79,7 @@ public class EmergencyAccessService : IEmergencyAccessService Email = emergencyContactEmail.ToLowerInvariant(), Status = EmergencyAccessStatusType.Invited, Type = accessType, - WaitTimeDays = waitTime, + WaitTimeDays = (short)waitTime, CreationDate = DateTime.UtcNow, RevisionDate = DateTime.UtcNow, }; diff --git a/src/Core/Billing/Extensions/InvoiceExtensions.cs b/src/Core/Billing/Extensions/InvoiceExtensions.cs index d62959c09a..774b6b93b2 100644 --- a/src/Core/Billing/Extensions/InvoiceExtensions.cs +++ b/src/Core/Billing/Extensions/InvoiceExtensions.cs @@ -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; diff --git a/src/Core/Billing/Organizations/Commands/UpdateOrganizationLicenseCommand.cs b/src/Core/Billing/Organizations/Commands/UpdateOrganizationLicenseCommand.cs index 1dfd786210..000edda1c2 100644 --- a/src/Core/Billing/Organizations/Commands/UpdateOrganizationLicenseCommand.cs +++ b/src/Core/Billing/Organizations/Commands/UpdateOrganizationLicenseCommand.cs @@ -1,9 +1,11 @@ using System.Text.Json; using Bit.Core.AdminConsole.Entities; +using Bit.Core.Billing.Enums; using Bit.Core.Billing.Licenses; using Bit.Core.Billing.Licenses.Extensions; using Bit.Core.Billing.Organizations.Models; using Bit.Core.Billing.Services; +using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Data.Organizations; using Bit.Core.Services; @@ -46,6 +48,57 @@ public class UpdateOrganizationLicenseCommand : IUpdateOrganizationLicenseComman } var claimsPrincipal = _licensingService.GetClaimsPrincipalFromLicense(license); + + // If the license has a Token (claims-based), extract all properties from claims BEFORE validation + // This ensures that CanUseLicense validation has access to the correct values from claims + // Otherwise, fall back to using the properties already on the license object (backward compatibility) + if (claimsPrincipal != null) + { + license.Name = claimsPrincipal.GetValue(OrganizationLicenseConstants.Name); + license.BillingEmail = claimsPrincipal.GetValue(OrganizationLicenseConstants.BillingEmail); + license.BusinessName = claimsPrincipal.GetValue(OrganizationLicenseConstants.BusinessName); + license.PlanType = claimsPrincipal.GetValue(OrganizationLicenseConstants.PlanType); + license.Seats = claimsPrincipal.GetValue(OrganizationLicenseConstants.Seats); + license.MaxCollections = claimsPrincipal.GetValue(OrganizationLicenseConstants.MaxCollections); + license.UsePolicies = claimsPrincipal.GetValue(OrganizationLicenseConstants.UsePolicies); + license.UseSso = claimsPrincipal.GetValue(OrganizationLicenseConstants.UseSso); + license.UseKeyConnector = claimsPrincipal.GetValue(OrganizationLicenseConstants.UseKeyConnector); + license.UseScim = claimsPrincipal.GetValue(OrganizationLicenseConstants.UseScim); + license.UseGroups = claimsPrincipal.GetValue(OrganizationLicenseConstants.UseGroups); + license.UseDirectory = claimsPrincipal.GetValue(OrganizationLicenseConstants.UseDirectory); + license.UseEvents = claimsPrincipal.GetValue(OrganizationLicenseConstants.UseEvents); + license.UseTotp = claimsPrincipal.GetValue(OrganizationLicenseConstants.UseTotp); + license.Use2fa = claimsPrincipal.GetValue(OrganizationLicenseConstants.Use2fa); + license.UseApi = claimsPrincipal.GetValue(OrganizationLicenseConstants.UseApi); + license.UseResetPassword = claimsPrincipal.GetValue(OrganizationLicenseConstants.UseResetPassword); + license.Plan = claimsPrincipal.GetValue(OrganizationLicenseConstants.Plan); + license.SelfHost = claimsPrincipal.GetValue(OrganizationLicenseConstants.SelfHost); + license.UsersGetPremium = claimsPrincipal.GetValue(OrganizationLicenseConstants.UsersGetPremium); + license.UseCustomPermissions = claimsPrincipal.GetValue(OrganizationLicenseConstants.UseCustomPermissions); + license.Enabled = claimsPrincipal.GetValue(OrganizationLicenseConstants.Enabled); + license.Expires = claimsPrincipal.GetValue(OrganizationLicenseConstants.Expires); + license.LicenseKey = claimsPrincipal.GetValue(OrganizationLicenseConstants.LicenseKey); + license.UsePasswordManager = claimsPrincipal.GetValue(OrganizationLicenseConstants.UsePasswordManager); + license.UseSecretsManager = claimsPrincipal.GetValue(OrganizationLicenseConstants.UseSecretsManager); + license.SmSeats = claimsPrincipal.GetValue(OrganizationLicenseConstants.SmSeats); + license.SmServiceAccounts = claimsPrincipal.GetValue(OrganizationLicenseConstants.SmServiceAccounts); + license.UseRiskInsights = claimsPrincipal.GetValue(OrganizationLicenseConstants.UseRiskInsights); + license.UseOrganizationDomains = claimsPrincipal.GetValue(OrganizationLicenseConstants.UseOrganizationDomains); + license.UseAdminSponsoredFamilies = claimsPrincipal.GetValue(OrganizationLicenseConstants.UseAdminSponsoredFamilies); + license.UseAutomaticUserConfirmation = claimsPrincipal.GetValue(OrganizationLicenseConstants.UseAutomaticUserConfirmation); + license.UseDisableSmAdsForUsers = claimsPrincipal.GetValue(OrganizationLicenseConstants.UseDisableSmAdsForUsers); + license.UsePhishingBlocker = claimsPrincipal.GetValue(OrganizationLicenseConstants.UsePhishingBlocker); + license.MaxStorageGb = claimsPrincipal.GetValue(OrganizationLicenseConstants.MaxStorageGb); + license.InstallationId = claimsPrincipal.GetValue(OrganizationLicenseConstants.InstallationId); + license.LicenseType = claimsPrincipal.GetValue(OrganizationLicenseConstants.LicenseType); + license.Issued = claimsPrincipal.GetValue(OrganizationLicenseConstants.Issued); + license.Refresh = claimsPrincipal.GetValue(OrganizationLicenseConstants.Refresh); + license.ExpirationWithoutGracePeriod = claimsPrincipal.GetValue(OrganizationLicenseConstants.ExpirationWithoutGracePeriod); + license.Trial = claimsPrincipal.GetValue(OrganizationLicenseConstants.Trial); + license.LimitCollectionCreationDeletion = claimsPrincipal.GetValue(OrganizationLicenseConstants.LimitCollectionCreationDeletion); + license.AllowAdminAccessToAllCollectionItems = claimsPrincipal.GetValue(OrganizationLicenseConstants.AllowAdminAccessToAllCollectionItems); + } + var canUse = license.CanUse(_globalSettings, _licensingService, claimsPrincipal, out var exception) && selfHostedOrganization.CanUseLicense(license, out exception); @@ -54,12 +107,6 @@ public class UpdateOrganizationLicenseCommand : IUpdateOrganizationLicenseComman throw new BadRequestException(exception); } - var useAutomaticUserConfirmation = claimsPrincipal? - .GetValue(OrganizationLicenseConstants.UseAutomaticUserConfirmation) ?? false; - - selfHostedOrganization.UseAutomaticUserConfirmation = useAutomaticUserConfirmation; - license.UseAutomaticUserConfirmation = useAutomaticUserConfirmation; - await WriteLicenseFileAsync(selfHostedOrganization, license); await UpdateOrganizationAsync(selfHostedOrganization, license); } diff --git a/src/Core/Billing/Premium/Commands/CreatePremiumCloudHostedSubscriptionCommand.cs b/src/Core/Billing/Premium/Commands/CreatePremiumCloudHostedSubscriptionCommand.cs index d52c79c1ee..764406ee56 100644 --- a/src/Core/Billing/Premium/Commands/CreatePremiumCloudHostedSubscriptionCommand.cs +++ b/src/Core/Billing/Premium/Commands/CreatePremiumCloudHostedSubscriptionCommand.cs @@ -361,7 +361,8 @@ public class CreatePremiumCloudHostedSubscriptionCommand( var invoice = await stripeAdapter.UpdateInvoiceAsync(subscription.LatestInvoiceId, new InvoiceUpdateOptions { - AutoAdvance = false + AutoAdvance = false, + Expand = ["customer"] }); await braintreeService.PayInvoice(new UserId(userId), invoice); diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 24e30fbcf0..6f42778b6b 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -142,8 +142,7 @@ public static class FeatureFlagKeys public const string PM23845_VNextApplicationCache = "pm-24957-refactor-memory-application-cache"; public const string BlockClaimedDomainAccountCreation = "pm-28297-block-uninvited-claimed-domain-registration"; public const string IncreaseBulkReinviteLimitForCloud = "pm-28251-increase-bulk-reinvite-limit-for-cloud"; - public const string BulkRevokeUsersV2 = "pm-28456-bulk-revoke-users-v2"; - public const string PremiumAccessQuery = "pm-21411-premium-access-query"; + public const string PremiumAccessQuery = "pm-29495-refactor-premium-interface"; /* Architecture */ public const string DesktopMigrationMilestone1 = "desktop-ui-migration-milestone-1"; @@ -165,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"; @@ -202,6 +202,7 @@ public static class FeatureFlagKeys public const string V2RegistrationTDEJIT = "pm-27279-v2-registration-tde-jit"; public const string DataRecoveryTool = "pm-28813-data-recovery-tool"; public const string EnableAccountEncryptionV2KeyConnectorRegistration = "enable-account-encryption-v2-key-connector-registration"; + public const string SdkKeyRotation = "pm-30144-sdk-key-rotation"; public const string EnableAccountEncryptionV2JitPasswordRegistration = "enable-account-encryption-v2-jit-password-registration"; /* Mobile Team */ @@ -229,23 +230,12 @@ public static class FeatureFlagKeys /// Enable this flag to share the send view used by the web and browser clients /// on the desktop client. /// - public const string DesktopSendUIRefresh = "desktop-send-ui-refresh"; public const string UseSdkPasswordGenerators = "pm-19976-use-sdk-password-generators"; public const string UseChromiumImporter = "pm-23982-chromium-importer"; public const string ChromiumImporterWithABE = "pm-25855-chromium-importer-abe"; public const string SendUIRefresh = "pm-28175-send-ui-refresh"; public const string SendEmailOTP = "pm-19051-send-email-verification"; - /// - /// Enable this flag to output email/OTP authenticated sends from the `GET sends` endpoint. When - /// this flag is disabled, the `GET sends` endpoint omits email/OTP authenticated sends. - /// - /// - /// This flag is server-side only, and only inhibits the endpoint returning all sends. - /// Email/OTP sends can still be created and downloaded through other endpoints. - /// - public const string PM19051_ListEmailOtpSends = "tools-send-email-otp-listing"; - /* Vault Team */ public const string CipherKeyEncryption = "cipher-key-encryption"; public const string PM19941MigrateCipherDomainToSdk = "pm-19941-migrate-cipher-domain-to-sdk"; diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 0783e84cc4..a423d9377d 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -3,6 +3,8 @@ false bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml + + $(WarningsNotAsErrors);CA1304;CA1305 @@ -44,7 +46,7 @@ - + diff --git a/src/Core/Entities/PlayItem.cs b/src/Core/Entities/PlayItem.cs new file mode 100644 index 0000000000..cf2f5c946b --- /dev/null +++ b/src/Core/Entities/PlayItem.cs @@ -0,0 +1,60 @@ +using System.ComponentModel.DataAnnotations; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Utilities; + +namespace Bit.Core.Entities; + +/// +/// PlayItem is a join table tracking entities created during automated testing. +/// A `PlayId` is supplied by the clients in the `x-play-id` header to inform the server +/// that any data created should be associated with the play, and therefore cleaned up with it. +/// +public class PlayItem : ITableObject +{ + public Guid Id { get; set; } + [MaxLength(256)] + public required string PlayId { get; init; } + public Guid? UserId { get; init; } + public Guid? OrganizationId { get; init; } + public DateTime CreationDate { get; init; } + + /// + /// Generates and sets a new COMB GUID for the Id property. + /// + public void SetNewId() + { + Id = CoreHelpers.GenerateComb(); + } + + /// + /// Creates a new PlayItem record associated with a User. + /// + /// The user entity created during the play. + /// The play identifier from the x-play-id header. + /// A new PlayItem instance tracking the user. + public static PlayItem Create(User user, string playId) + { + return new PlayItem + { + PlayId = playId, + UserId = user.Id, + CreationDate = DateTime.UtcNow + }; + } + + /// + /// Creates a new PlayItem record associated with an Organization. + /// + /// The organization entity created during the play. + /// The play identifier from the x-play-id header. + /// A new PlayItem instance tracking the organization. + public static PlayItem Create(Organization organization, string playId) + { + return new PlayItem + { + PlayId = playId, + OrganizationId = organization.Id, + CreationDate = DateTime.UtcNow + }; + } +} diff --git a/src/Core/MailTemplates/Mjml/emails/AdminConsole/OrganizationConfirmation/organization-confirmation-enterprise-teams.mjml b/src/Core/MailTemplates/Mjml/emails/AdminConsole/OrganizationConfirmation/organization-confirmation-enterprise-teams.mjml index 6d3c46ae67..b94bf0dc86 100644 --- a/src/Core/MailTemplates/Mjml/emails/AdminConsole/OrganizationConfirmation/organization-confirmation-enterprise-teams.mjml +++ b/src/Core/MailTemplates/Mjml/emails/AdminConsole/OrganizationConfirmation/organization-confirmation-enterprise-teams.mjml @@ -8,8 +8,8 @@ @@ -33,7 +33,7 @@ icon-alt="Group Users Icon" text="You can easily access and share passwords with your team." foot-url-text="Share passwords in Bitwarden" - foot-url="https://bitwarden.com/help/share-to-a-collection/" + foot-url="https://bitwarden.com/help/sharing" /> diff --git a/src/Core/MailTemplates/Mjml/emails/AdminConsole/OrganizationConfirmation/organization-confirmation-family-free.mjml b/src/Core/MailTemplates/Mjml/emails/AdminConsole/OrganizationConfirmation/organization-confirmation-family-free.mjml index 2b2d854134..c223e2f650 100644 --- a/src/Core/MailTemplates/Mjml/emails/AdminConsole/OrganizationConfirmation/organization-confirmation-family-free.mjml +++ b/src/Core/MailTemplates/Mjml/emails/AdminConsole/OrganizationConfirmation/organization-confirmation-family-free.mjml @@ -8,8 +8,8 @@ @@ -33,7 +33,7 @@ icon-alt="Group Users Icon" text="You can easily share passwords with friends, family, or coworkers." foot-url-text="Share passwords in Bitwarden" - foot-url="https://bitwarden.com/help/share-to-a-collection/" + foot-url="https://bitwarden.com/help/sharing" /> diff --git a/src/Core/Repositories/IPlayItemRepository.cs b/src/Core/Repositories/IPlayItemRepository.cs new file mode 100644 index 0000000000..fb00be2bc1 --- /dev/null +++ b/src/Core/Repositories/IPlayItemRepository.cs @@ -0,0 +1,11 @@ +using Bit.Core.Entities; + +#nullable enable + +namespace Bit.Core.Repositories; + +public interface IPlayItemRepository : IRepository +{ + Task> GetByPlayIdAsync(string playId); + Task DeleteByPlayIdAsync(string playId); +} diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 763f70dd0c..64caf1d462 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -14,6 +14,8 @@ using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; +using Bit.Core.Billing.Licenses; +using Bit.Core.Billing.Licenses.Extensions; using Bit.Core.Billing.Models; using Bit.Core.Billing.Models.Business; using Bit.Core.Billing.Models.Sales; @@ -982,6 +984,16 @@ public class UserService : UserManager, IUserService throw new BadRequestException(exceptionMessage); } + // If the license has a Token (claims-based), extract all properties from claims + // Otherwise, fall back to using the properties already on the license object (backward compatibility) + if (claimsPrincipal != null) + { + license.LicenseKey = claimsPrincipal.GetValue(UserLicenseConstants.LicenseKey); + license.Premium = claimsPrincipal.GetValue(UserLicenseConstants.Premium); + license.MaxStorageGb = claimsPrincipal.GetValue(UserLicenseConstants.MaxStorageGb); + license.Expires = claimsPrincipal.GetValue(UserLicenseConstants.Expires); + } + var dir = $"{_globalSettings.LicenseDirectory}/user"; Directory.CreateDirectory(dir); using var fs = File.OpenWrite(Path.Combine(dir, $"{user.Id}.json")); diff --git a/src/Core/Services/Play/IPlayIdService.cs b/src/Core/Services/Play/IPlayIdService.cs new file mode 100644 index 0000000000..542de9725f --- /dev/null +++ b/src/Core/Services/Play/IPlayIdService.cs @@ -0,0 +1,23 @@ +namespace Bit.Core.Services; + +/// +/// Service for managing Play identifiers in automated testing infrastructure. +/// A "Play" is a test session that groups entities created during testing to enable cleanup. +/// The PlayId flows from client request (x-play-id header) through PlayIdMiddleware to this service, +/// which repositories query to create PlayItem tracking records via IPlayItemService. The SeederAPI uses these records +/// to bulk delete all entities associated with a PlayId. Only active in Development environments. +/// +public interface IPlayIdService +{ + /// + /// Gets or sets the current Play identifier from the x-play-id request header. + /// + string? PlayId { get; set; } + + /// + /// Checks whether the current request is part of an active Play session. + /// + /// The Play identifier if active, otherwise empty string. + /// True if in a Play session (has PlayId and in Development environment), otherwise false. + bool InPlay(out string playId); +} diff --git a/src/Core/Services/Play/IPlayItemService.cs b/src/Core/Services/Play/IPlayItemService.cs new file mode 100644 index 0000000000..5033aaf626 --- /dev/null +++ b/src/Core/Services/Play/IPlayItemService.cs @@ -0,0 +1,27 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Entities; + +namespace Bit.Core.Services; + +/// +/// Service used to track added users and organizations during a Play session. +/// +public interface IPlayItemService +{ + /// + /// Records a PlayItem entry for the given User created during a Play session. + /// + /// Does nothing if no Play Id is set for this http scope. + /// + /// + /// + Task Record(User user); + /// + /// Records a PlayItem entry for the given Organization created during a Play session. + /// + /// Does nothing if no Play Id is set for this http scope. + /// + /// + /// + Task Record(Organization organization); +} diff --git a/src/Core/Services/Play/Implementations/NeverPlayIdServices.cs b/src/Core/Services/Play/Implementations/NeverPlayIdServices.cs new file mode 100644 index 0000000000..baab44eedb --- /dev/null +++ b/src/Core/Services/Play/Implementations/NeverPlayIdServices.cs @@ -0,0 +1,16 @@ +namespace Bit.Core.Services; + +public class NeverPlayIdServices : IPlayIdService +{ + public string? PlayId + { + get => null; + set { } + } + + public bool InPlay(out string playId) + { + playId = string.Empty; + return false; + } +} diff --git a/src/Core/Services/Play/Implementations/PlayIdService.cs b/src/Core/Services/Play/Implementations/PlayIdService.cs new file mode 100644 index 0000000000..0c64f1dd14 --- /dev/null +++ b/src/Core/Services/Play/Implementations/PlayIdService.cs @@ -0,0 +1,13 @@ +using Microsoft.Extensions.Hosting; + +namespace Bit.Core.Services; + +public class PlayIdService(IHostEnvironment hostEnvironment) : IPlayIdService +{ + public string? PlayId { get; set; } + public bool InPlay(out string playId) + { + playId = PlayId ?? string.Empty; + return !string.IsNullOrEmpty(PlayId) && hostEnvironment.IsDevelopment(); + } +} diff --git a/src/Core/Services/Play/Implementations/PlayIdSingletonService.cs b/src/Core/Services/Play/Implementations/PlayIdSingletonService.cs new file mode 100644 index 0000000000..30bb30aad2 --- /dev/null +++ b/src/Core/Services/Play/Implementations/PlayIdSingletonService.cs @@ -0,0 +1,48 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Bit.Core.Services; + +/// +/// Singleton wrapper service that bridges singleton-scoped service boundaries for PlayId tracking. +/// This allows singleton services to access the scoped PlayIdService via HttpContext.RequestServices. +/// +/// Uses IHttpContextAccessor to retrieve the current request's scoped PlayIdService instance, enabling +/// singleton services to participate in Play session tracking without violating DI lifetime rules. +/// Falls back to NeverPlayIdServices when no HttpContext is available (e.g., background jobs). +/// +public class PlayIdSingletonService(IHttpContextAccessor httpContextAccessor, IHostEnvironment hostEnvironment) : IPlayIdService +{ + private IPlayIdService Current + { + get + { + var httpContext = httpContextAccessor.HttpContext; + if (httpContext == null) + { + return new NeverPlayIdServices(); + } + return httpContext.RequestServices.GetRequiredService(); + } + } + + public string? PlayId + { + get => Current.PlayId; + set => Current.PlayId = value; + } + + public bool InPlay(out string playId) + { + if (hostEnvironment.IsDevelopment()) + { + return Current.InPlay(out playId); + } + else + { + playId = string.Empty; + return false; + } + } +} diff --git a/src/Core/Services/Play/Implementations/PlayItemService.cs b/src/Core/Services/Play/Implementations/PlayItemService.cs new file mode 100644 index 0000000000..981445f2ea --- /dev/null +++ b/src/Core/Services/Play/Implementations/PlayItemService.cs @@ -0,0 +1,26 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Entities; +using Bit.Core.Repositories; +using Microsoft.Extensions.Logging; + +namespace Bit.Core.Services; + +public class PlayItemService(IPlayIdService playIdService, IPlayItemRepository playItemRepository, ILogger logger) : IPlayItemService +{ + public async Task Record(User user) + { + if (playIdService.InPlay(out var playId)) + { + logger.LogInformation("Associating user {UserId} with Play ID {PlayId}", user.Id, playId); + await playItemRepository.CreateAsync(PlayItem.Create(user, playId)); + } + } + public async Task Record(Organization organization) + { + if (playIdService.InPlay(out var playId)) + { + logger.LogInformation("Associating organization {OrganizationId} with Play ID {PlayId}", organization.Id, playId); + await playItemRepository.CreateAsync(PlayItem.Create(organization, playId)); + } + } +} diff --git a/src/Core/Services/Play/README.md b/src/Core/Services/Play/README.md new file mode 100644 index 0000000000..b04b6a13c7 --- /dev/null +++ b/src/Core/Services/Play/README.md @@ -0,0 +1,27 @@ +# Play Services + +## Overview + +The Play services provide automated testing infrastructure for tracking and cleaning up test data in development +environments. A "Play" is a test session that groups entities (users, organizations, etc.) created during testing to +enable bulk cleanup via the SeederAPI. + +## How It Works + +1. Test client sends `x-play-id` header with a unique Play identifier +2. `PlayIdMiddleware` extracts the header and sets it on `IPlayIdService` +3. Repositories check `IPlayIdService.InPlay()` when creating entities +4. `IPlayItemService` records PlayItem entries for tracked entities +5. SeederAPI uses PlayItem records to bulk delete all entities associated with a PlayId + +Play services are **only active in Development environments**. + +## Classes + +- **`IPlayIdService`** - Interface for managing Play identifiers in the current request scope +- **`IPlayItemService`** - Interface for tracking entities created during a Play session +- **`PlayIdService`** - Default scoped implementation for tracking Play sessions per HTTP request +- **`NeverPlayIdServices`** - No-op implementation used as fallback when no HttpContext is available +- **`PlayIdSingletonService`** - Singleton wrapper that allows singleton services to access scoped PlayIdService via + HttpContext +- **`PlayItemService`** - Implementation that records PlayItem entries for entities created during Play sessions diff --git a/src/Core/Settings/GlobalSettings.cs b/src/Core/Settings/GlobalSettings.cs index 60a1fda19f..1f4fa6104b 100644 --- a/src/Core/Settings/GlobalSettings.cs +++ b/src/Core/Settings/GlobalSettings.cs @@ -44,6 +44,7 @@ public class GlobalSettings : IGlobalSettings public virtual bool EnableCloudCommunication { get; set; } = false; public virtual int OrganizationInviteExpirationHours { get; set; } = 120; // 5 days public virtual string EventGridKey { get; set; } + public virtual bool TestPlayIdTrackingEnabled { get; set; } = false; public virtual IInstallationSettings Installation { get; set; } = new InstallationSettings(); public virtual IBaseServiceUriSettings BaseServiceUri { get; set; } public virtual string DatabaseProvider { get; set; } diff --git a/src/Core/Tools/Enums/AuthType.cs b/src/Core/Tools/Enums/AuthType.cs index 814ebf69b8..4a31275b7a 100644 --- a/src/Core/Tools/Enums/AuthType.cs +++ b/src/Core/Tools/Enums/AuthType.cs @@ -1,11 +1,8 @@ -using System.Text.Json.Serialization; - -namespace Bit.Core.Tools.Enums; +namespace Bit.Core.Tools.Enums; /// /// Specifies the authentication method required to access a Send. /// -[JsonConverter(typeof(JsonStringEnumConverter))] public enum AuthType : byte { /// diff --git a/src/Core/Tools/SendFeatures/Queries/SendAuthenticationQuery.cs b/src/Core/Tools/SendFeatures/Queries/SendAuthenticationQuery.cs index fed7c9e8d4..97c2e64dc5 100644 --- a/src/Core/Tools/SendFeatures/Queries/SendAuthenticationQuery.cs +++ b/src/Core/Tools/SendFeatures/Queries/SendAuthenticationQuery.cs @@ -1,4 +1,5 @@ -using Bit.Core.Tools.Models.Data; +using Bit.Core.Tools.Enums; +using Bit.Core.Tools.Models.Data; using Bit.Core.Tools.Repositories; using Bit.Core.Tools.SendFeatures.Queries.Interfaces; @@ -37,8 +38,8 @@ public class SendAuthenticationQuery : ISendAuthenticationQuery { null => NEVER_AUTHENTICATE, var s when s.AccessCount >= s.MaxAccessCount => NEVER_AUTHENTICATE, - var s when s.Emails is not null => emailOtp(s.Emails), - var s when s.Password is not null => new ResourcePassword(s.Password), + var s when s.AuthType == AuthType.Email && s.Emails is not null => emailOtp(s.Emails), + var s when s.AuthType == AuthType.Password && s.Password is not null => new ResourcePassword(s.Password), _ => NOT_AUTHENTICATED }; diff --git a/src/Core/Tools/SendFeatures/Queries/SendOwnerQuery.cs b/src/Core/Tools/SendFeatures/Queries/SendOwnerQuery.cs index cb539429a5..29bd8f56f9 100644 --- a/src/Core/Tools/SendFeatures/Queries/SendOwnerQuery.cs +++ b/src/Core/Tools/SendFeatures/Queries/SendOwnerQuery.cs @@ -12,7 +12,6 @@ namespace Bit.Core.Tools.SendFeatures.Queries; public class SendOwnerQuery : ISendOwnerQuery { private readonly ISendRepository _repository; - private readonly IFeatureService _features; private readonly IUserService _users; /// @@ -24,10 +23,9 @@ public class SendOwnerQuery : ISendOwnerQuery /// /// Thrown when is . /// - public SendOwnerQuery(ISendRepository sendRepository, IFeatureService features, IUserService users) + public SendOwnerQuery(ISendRepository sendRepository, IUserService users) { _repository = sendRepository; - _features = features ?? throw new ArgumentNullException(nameof(features)); _users = users ?? throw new ArgumentNullException(nameof(users)); } @@ -51,16 +49,6 @@ public class SendOwnerQuery : ISendOwnerQuery var userId = _users.GetProperUserId(user) ?? throw new BadRequestException("invalid user."); var sends = await _repository.GetManyByUserIdAsync(userId); - var removeEmailOtp = !_features.IsEnabled(FeatureFlagKeys.PM19051_ListEmailOtpSends); - if (removeEmailOtp) - { - // reify list to avoid invalidating the enumerator - foreach (var s in sends.Where(s => s.Emails != null).ToList()) - { - sends.Remove(s); - } - } - return sends; } } diff --git a/src/Events/Startup.cs b/src/Events/Startup.cs index 75301cf08c..d97d65c2ed 100644 --- a/src/Events/Startup.cs +++ b/src/Events/Startup.cs @@ -36,6 +36,7 @@ public class Startup // Repositories services.AddDatabaseRepositories(globalSettings); + services.AddTestPlayIdTracking(globalSettings); // Context services.AddScoped(); diff --git a/src/EventsProcessor/Startup.cs b/src/EventsProcessor/Startup.cs index 888dda43a1..239393a693 100644 --- a/src/EventsProcessor/Startup.cs +++ b/src/EventsProcessor/Startup.cs @@ -30,6 +30,7 @@ public class Startup // Repositories services.AddDatabaseRepositories(globalSettings); + services.AddTestPlayIdTracking(globalSettings); // Add event integration services services.AddDistributedCache(globalSettings); diff --git a/src/Icons/Icons.csproj b/src/Icons/Icons.csproj index 455c8b3155..97e9562183 100644 --- a/src/Icons/Icons.csproj +++ b/src/Icons/Icons.csproj @@ -3,6 +3,8 @@ bitwarden-Icons false + + $(WarningsNotAsErrors);CA1304;CA1305 diff --git a/src/Identity/Identity.csproj b/src/Identity/Identity.csproj index bf5ab82166..db49f8c856 100644 --- a/src/Identity/Identity.csproj +++ b/src/Identity/Identity.csproj @@ -3,6 +3,8 @@ bitwarden-Identity false + + $(WarningsNotAsErrors);CA1305 diff --git a/src/Identity/Startup.cs b/src/Identity/Startup.cs index 5dc443a73c..9d5536fd10 100644 --- a/src/Identity/Startup.cs +++ b/src/Identity/Startup.cs @@ -49,6 +49,7 @@ public class Startup // Repositories services.AddDatabaseRepositories(globalSettings); + services.AddTestPlayIdTracking(globalSettings); // Context services.AddScoped(); diff --git a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationRepository.cs b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationRepository.cs index 96ddc8c7da..434edb7676 100644 --- a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationRepository.cs +++ b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationRepository.cs @@ -17,7 +17,7 @@ namespace Bit.Infrastructure.Dapper.Repositories; public class OrganizationRepository : Repository, IOrganizationRepository { - private readonly ILogger _logger; + protected readonly ILogger _logger; public OrganizationRepository( GlobalSettings globalSettings, diff --git a/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs b/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs index e3ee82270f..dcb0dc1306 100644 --- a/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs +++ b/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs @@ -51,6 +51,7 @@ public static class DapperServiceCollectionExtensions services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/src/Infrastructure.Dapper/Infrastructure.Dapper.csproj b/src/Infrastructure.Dapper/Infrastructure.Dapper.csproj index 8feb455feb..d87bdc33a9 100644 --- a/src/Infrastructure.Dapper/Infrastructure.Dapper.csproj +++ b/src/Infrastructure.Dapper/Infrastructure.Dapper.csproj @@ -1,5 +1,10 @@ + + + $(WarningsNotAsErrors);CA1305 + + diff --git a/src/Infrastructure.Dapper/Repositories/PlayItemRepository.cs b/src/Infrastructure.Dapper/Repositories/PlayItemRepository.cs new file mode 100644 index 0000000000..1fa8e88ce8 --- /dev/null +++ b/src/Infrastructure.Dapper/Repositories/PlayItemRepository.cs @@ -0,0 +1,45 @@ +using System.Data; +using Bit.Core.Entities; +using Bit.Core.Repositories; +using Bit.Core.Settings; +using Dapper; +using Microsoft.Data.SqlClient; + +#nullable enable + +namespace Bit.Infrastructure.Dapper.Repositories; + +public class PlayItemRepository : Repository, IPlayItemRepository +{ + public PlayItemRepository(GlobalSettings globalSettings) + : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString) + { } + + public PlayItemRepository(string connectionString, string readOnlyConnectionString) + : base(connectionString, readOnlyConnectionString) + { } + + public async Task> GetByPlayIdAsync(string playId) + { + using (var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.QueryAsync( + "[dbo].[PlayItem_ReadByPlayId]", + new { PlayId = playId }, + commandType: CommandType.StoredProcedure); + + return results.ToList(); + } + } + + public async Task DeleteByPlayIdAsync(string playId) + { + using (var connection = new SqlConnection(ConnectionString)) + { + await connection.ExecuteAsync( + "[dbo].[PlayItem_DeleteByPlayId]", + new { PlayId = playId }, + commandType: CommandType.StoredProcedure); + } + } +} diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs index 89f0bb5806..88410facf5 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs @@ -20,7 +20,7 @@ namespace Bit.Infrastructure.EntityFramework.Repositories; public class OrganizationRepository : Repository, IOrganizationRepository { - private readonly ILogger _logger; + protected readonly ILogger _logger; public OrganizationRepository( IServiceScopeFactory serviceScopeFactory, diff --git a/src/Infrastructure.EntityFramework/Configurations/PlayItemEntityTypeConfiguration.cs b/src/Infrastructure.EntityFramework/Configurations/PlayItemEntityTypeConfiguration.cs new file mode 100644 index 0000000000..91b26e5be4 --- /dev/null +++ b/src/Infrastructure.EntityFramework/Configurations/PlayItemEntityTypeConfiguration.cs @@ -0,0 +1,46 @@ +using Bit.Infrastructure.EntityFramework.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Bit.Infrastructure.EntityFramework.Configurations; + +public class PlayItemEntityTypeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder + .Property(pd => pd.Id) + .ValueGeneratedNever(); + + builder + .HasIndex(pd => pd.PlayId) + .IsClustered(false); + + builder + .HasIndex(pd => pd.UserId) + .IsClustered(false); + + builder + .HasIndex(pd => pd.OrganizationId) + .IsClustered(false); + + builder + .HasOne(pd => pd.User) + .WithMany() + .HasForeignKey(pd => pd.UserId) + .OnDelete(DeleteBehavior.Cascade); + + builder + .HasOne(pd => pd.Organization) + .WithMany() + .HasForeignKey(pd => pd.OrganizationId) + .OnDelete(DeleteBehavior.Cascade); + + builder + .ToTable(nameof(PlayItem)) + .HasCheckConstraint( + "CK_PlayItem_UserOrOrganization", + "(\"UserId\" IS NOT NULL AND \"OrganizationId\" IS NULL) OR (\"UserId\" IS NULL AND \"OrganizationId\" IS NOT NULL)" + ); + } +} diff --git a/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs b/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs index 3c35df2a82..320cb9436d 100644 --- a/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs +++ b/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs @@ -88,6 +88,7 @@ public static class EntityFrameworkServiceCollectionExtensions services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/src/Infrastructure.EntityFramework/Infrastructure.EntityFramework.csproj b/src/Infrastructure.EntityFramework/Infrastructure.EntityFramework.csproj index 9814eef2aa..180bcd7705 100644 --- a/src/Infrastructure.EntityFramework/Infrastructure.EntityFramework.csproj +++ b/src/Infrastructure.EntityFramework/Infrastructure.EntityFramework.csproj @@ -1,4 +1,9 @@ + + + $(WarningsNotAsErrors);CA1304;CA1305 + + diff --git a/src/Infrastructure.EntityFramework/Models/PlayItem.cs b/src/Infrastructure.EntityFramework/Models/PlayItem.cs new file mode 100644 index 0000000000..3aafebd555 --- /dev/null +++ b/src/Infrastructure.EntityFramework/Models/PlayItem.cs @@ -0,0 +1,19 @@ +#nullable enable + +using AutoMapper; + +namespace Bit.Infrastructure.EntityFramework.Models; + +public class PlayItem : Core.Entities.PlayItem +{ + public virtual User? User { get; set; } + public virtual AdminConsole.Models.Organization? Organization { get; set; } +} + +public class PlayItemMapperProfile : Profile +{ + public PlayItemMapperProfile() + { + CreateMap().ReverseMap(); + } +} diff --git a/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs b/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs index b748a26db2..7b67a63912 100644 --- a/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs +++ b/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs @@ -57,6 +57,7 @@ public class DatabaseContext : DbContext public DbSet OrganizationApiKeys { get; set; } public DbSet OrganizationSponsorships { get; set; } public DbSet OrganizationConnections { get; set; } + public DbSet PlayItem { get; set; } public DbSet OrganizationIntegrations { get; set; } public DbSet OrganizationIntegrationConfigurations { get; set; } public DbSet OrganizationUsers { get; set; } diff --git a/src/Infrastructure.EntityFramework/Repositories/PlayItemRepository.cs b/src/Infrastructure.EntityFramework/Repositories/PlayItemRepository.cs new file mode 100644 index 0000000000..c8cf207b43 --- /dev/null +++ b/src/Infrastructure.EntityFramework/Repositories/PlayItemRepository.cs @@ -0,0 +1,42 @@ +using AutoMapper; +using Bit.Core.Repositories; +using Bit.Infrastructure.EntityFramework.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +#nullable enable + +namespace Bit.Infrastructure.EntityFramework.Repositories; + +public class PlayItemRepository : Repository, IPlayItemRepository +{ + public PlayItemRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper) + : base(serviceScopeFactory, mapper, (DatabaseContext context) => context.PlayItem) + { } + + public async Task> GetByPlayIdAsync(string playId) + { + using (var scope = ServiceScopeFactory.CreateScope()) + { + var dbContext = GetDatabaseContext(scope); + var playItemEntities = await GetDbSet(dbContext) + .Where(pd => pd.PlayId == playId) + .ToListAsync(); + return Mapper.Map>(playItemEntities); + } + } + + public async Task DeleteByPlayIdAsync(string playId) + { + using (var scope = ServiceScopeFactory.CreateScope()) + { + var dbContext = GetDatabaseContext(scope); + var entities = await GetDbSet(dbContext) + .Where(pd => pd.PlayId == playId) + .ToListAsync(); + + dbContext.PlayItem.RemoveRange(entities); + await dbContext.SaveChangesAsync(); + } + } +} diff --git a/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs b/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs index cb9e8f3f13..7b67e4c620 100644 --- a/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs +++ b/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs @@ -801,7 +801,7 @@ public class CipherRepository : Repository + /// Adds PlayId tracking decorators for User and Organization repositories using Dapper implementations. + /// This replaces the standard repository implementations with tracking versions + /// that record created entities for test data cleanup. Only call when TestPlayIdTrackingEnabled is true. + /// + public static void AddPlayIdTrackingDapperRepositories(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + } + + /// + /// Adds PlayId tracking decorators for User and Organization repositories using EntityFramework implementations. + /// This replaces the standard repository implementations with tracking versions + /// that record created entities for test data cleanup. Only call when TestPlayIdTrackingEnabled is true. + /// + public static void AddPlayIdTrackingEFRepositories(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + } +} diff --git a/src/SharedWeb/Play/Repositories/DapperTestOrganizationTrackingOrganizationRepository.cs b/src/SharedWeb/Play/Repositories/DapperTestOrganizationTrackingOrganizationRepository.cs new file mode 100644 index 0000000000..0d11d9c5eb --- /dev/null +++ b/src/SharedWeb/Play/Repositories/DapperTestOrganizationTrackingOrganizationRepository.cs @@ -0,0 +1,32 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Services; +using Bit.Core.Settings; +using Bit.Infrastructure.Dapper.Repositories; +using Microsoft.Extensions.Logging; + +namespace Bit.SharedWeb.Play.Repositories; + +/// +/// Dapper decorator around the that tracks +/// created Organizations for seeding. +/// +public class DapperTestOrganizationTrackingOrganizationRepository : OrganizationRepository +{ + private readonly IPlayItemService _playItemService; + + public DapperTestOrganizationTrackingOrganizationRepository( + IPlayItemService playItemService, + GlobalSettings globalSettings, + ILogger logger) + : base(globalSettings, logger) + { + _playItemService = playItemService; + } + + public override async Task CreateAsync(Organization obj) + { + var createdOrganization = await base.CreateAsync(obj); + await _playItemService.Record(createdOrganization); + return createdOrganization; + } +} diff --git a/src/SharedWeb/Play/Repositories/DapperTestUserTrackingUserRepository.cs b/src/SharedWeb/Play/Repositories/DapperTestUserTrackingUserRepository.cs new file mode 100644 index 0000000000..97954c0348 --- /dev/null +++ b/src/SharedWeb/Play/Repositories/DapperTestUserTrackingUserRepository.cs @@ -0,0 +1,33 @@ +using Bit.Core.Entities; +using Bit.Core.Services; +using Bit.Core.Settings; +using Bit.Infrastructure.Dapper.Repositories; +using Microsoft.AspNetCore.DataProtection; + +namespace Bit.SharedWeb.Play.Repositories; + +/// +/// Dapper decorator around the that tracks +/// created Users for seeding. +/// +public class DapperTestUserTrackingUserRepository : UserRepository +{ + private readonly IPlayItemService _playItemService; + + public DapperTestUserTrackingUserRepository( + IPlayItemService playItemService, + GlobalSettings globalSettings, + IDataProtectionProvider dataProtectionProvider) + : base(globalSettings, dataProtectionProvider) + { + _playItemService = playItemService; + } + + public override async Task CreateAsync(User user) + { + var createdUser = await base.CreateAsync(user); + + await _playItemService.Record(createdUser); + return createdUser; + } +} diff --git a/src/SharedWeb/Play/Repositories/EFTestOrganizationTrackingOrganizationRepository.cs b/src/SharedWeb/Play/Repositories/EFTestOrganizationTrackingOrganizationRepository.cs new file mode 100644 index 0000000000..fdf0e4d685 --- /dev/null +++ b/src/SharedWeb/Play/Repositories/EFTestOrganizationTrackingOrganizationRepository.cs @@ -0,0 +1,33 @@ +using AutoMapper; +using Bit.Core.Services; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Bit.SharedWeb.Play.Repositories; + +/// +/// EntityFramework decorator around the that tracks +/// created Organizations for seeding. +/// +public class EFTestOrganizationTrackingOrganizationRepository : OrganizationRepository +{ + private readonly IPlayItemService _playItemService; + + public EFTestOrganizationTrackingOrganizationRepository( + IPlayItemService playItemService, + IServiceScopeFactory serviceScopeFactory, + IMapper mapper, + ILogger logger) + : base(serviceScopeFactory, mapper, logger) + { + _playItemService = playItemService; + } + + public override async Task CreateAsync(Core.AdminConsole.Entities.Organization organization) + { + var createdOrganization = await base.CreateAsync(organization); + await _playItemService.Record(createdOrganization); + return createdOrganization; + } +} diff --git a/src/SharedWeb/Play/Repositories/EFTestUserTrackingUserRepository.cs b/src/SharedWeb/Play/Repositories/EFTestUserTrackingUserRepository.cs new file mode 100644 index 0000000000..bafcd17ba0 --- /dev/null +++ b/src/SharedWeb/Play/Repositories/EFTestUserTrackingUserRepository.cs @@ -0,0 +1,31 @@ +using AutoMapper; +using Bit.Core.Services; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.Extensions.DependencyInjection; + +namespace Bit.SharedWeb.Play.Repositories; + +/// +/// EntityFramework decorator around the that tracks +/// created Users for seeding. +/// +public class EFTestUserTrackingUserRepository : UserRepository +{ + private readonly IPlayItemService _playItemService; + + public EFTestUserTrackingUserRepository( + IPlayItemService playItemService, + IServiceScopeFactory serviceScopeFactory, + IMapper mapper) + : base(serviceScopeFactory, mapper) + { + _playItemService = playItemService; + } + + public override async Task CreateAsync(Core.Entities.User user) + { + var createdUser = await base.CreateAsync(user); + await _playItemService.Record(createdUser); + return createdUser; + } +} diff --git a/src/SharedWeb/SharedWeb.csproj b/src/SharedWeb/SharedWeb.csproj index d8dc61178d..b6036845b0 100644 --- a/src/SharedWeb/SharedWeb.csproj +++ b/src/SharedWeb/SharedWeb.csproj @@ -1,5 +1,10 @@ + + + $(WarningsNotAsErrors);CA1304 + + diff --git a/src/SharedWeb/Utilities/PlayIdMiddleware.cs b/src/SharedWeb/Utilities/PlayIdMiddleware.cs new file mode 100644 index 0000000000..c00ab2b657 --- /dev/null +++ b/src/SharedWeb/Utilities/PlayIdMiddleware.cs @@ -0,0 +1,41 @@ +using Bit.Core.Services; +using Microsoft.AspNetCore.Http; + +namespace Bit.SharedWeb.Utilities; + +/// +/// Middleware to extract the x-play-id header and set it in the PlayIdService. +/// +/// PlayId is used in testing infrastructure to track data created during automated testing and fa cilitate cleanup. +/// +/// +public sealed class PlayIdMiddleware(RequestDelegate next) +{ + private const int MaxPlayIdLength = 256; + + public async Task Invoke(HttpContext context, PlayIdService playIdService) + { + if (context.Request.Headers.TryGetValue("x-play-id", out var playId)) + { + var playIdValue = playId.ToString(); + + if (string.IsNullOrWhiteSpace(playIdValue)) + { + context.Response.StatusCode = StatusCodes.Status400BadRequest; + await context.Response.WriteAsJsonAsync(new { Error = "x-play-id header cannot be empty or whitespace" }); + return; + } + + if (playIdValue.Length > MaxPlayIdLength) + { + context.Response.StatusCode = StatusCodes.Status400BadRequest; + await context.Response.WriteAsJsonAsync(new { Error = $"x-play-id header cannot exceed {MaxPlayIdLength} characters" }); + return; + } + + playIdService.PlayId = playIdValue; + } + + await next(context); + } +} diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index 8f5dfdf3f4..1bb9cb6c7a 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -56,6 +56,7 @@ using Bit.Core.Vault; using Bit.Core.Vault.Services; using Bit.Infrastructure.Dapper; using Bit.Infrastructure.EntityFramework; +using Bit.SharedWeb.Play; using DnsClient; using Duende.IdentityModel; using LaunchDarkly.Sdk.Server; @@ -117,6 +118,40 @@ public static class ServiceCollectionExtensions return provider; } + /// + /// Registers test PlayId tracking services for test data management and cleanup. + /// This infrastructure is isolated to test environments and enables tracking of test-generated entities. + /// + public static void AddTestPlayIdTracking(this IServiceCollection services, GlobalSettings globalSettings) + { + if (globalSettings.TestPlayIdTrackingEnabled) + { + var (provider, _) = GetDatabaseProvider(globalSettings); + + // Include PlayIdService for tracking Play Ids in repositories + // We need the http context accessor to use the Singleton version, which pulls from the scoped version + services.AddHttpContextAccessor(); + + services.AddSingleton(); + services.AddSingleton(); + services.AddScoped(); + + // Replace standard repositories with PlayId tracking decorators + if (provider == SupportedDatabaseProviders.SqlServer) + { + services.AddPlayIdTrackingDapperRepositories(); + } + else + { + services.AddPlayIdTrackingEFRepositories(); + } + } + else + { + services.AddSingleton(); + } + } + public static void AddBaseServices(this IServiceCollection services, IGlobalSettings globalSettings) { services.AddScoped(); @@ -522,6 +557,10 @@ public static class ServiceCollectionExtensions IWebHostEnvironment env, GlobalSettings globalSettings) { app.UseMiddleware(); + if (globalSettings.TestPlayIdTrackingEnabled) + { + app.UseMiddleware(); + } } public static void UseForwardedHeaders(this IApplicationBuilder app, IGlobalSettings globalSettings) diff --git a/src/Sql/dbo/Stored Procedures/PlayItem_Create.sql b/src/Sql/dbo/Stored Procedures/PlayItem_Create.sql new file mode 100644 index 0000000000..cf75d03d10 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/PlayItem_Create.sql @@ -0,0 +1,27 @@ +CREATE PROCEDURE [dbo].[PlayItem_Create] + @Id UNIQUEIDENTIFIER OUTPUT, + @PlayId NVARCHAR(256), + @UserId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @CreationDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[PlayItem] + ( + [Id], + [PlayId], + [UserId], + [OrganizationId], + [CreationDate] + ) + VALUES + ( + @Id, + @PlayId, + @UserId, + @OrganizationId, + @CreationDate + ) +END diff --git a/src/Sql/dbo/Stored Procedures/PlayItem_DeleteByPlayId.sql b/src/Sql/dbo/Stored Procedures/PlayItem_DeleteByPlayId.sql new file mode 100644 index 0000000000..5e2a102e40 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/PlayItem_DeleteByPlayId.sql @@ -0,0 +1,12 @@ +CREATE PROCEDURE [dbo].[PlayItem_DeleteByPlayId] + @PlayId NVARCHAR(256) +AS +BEGIN + SET NOCOUNT ON + + DELETE + FROM + [dbo].[PlayItem] + WHERE + [PlayId] = @PlayId +END diff --git a/src/Sql/dbo/Stored Procedures/PlayItem_ReadByPlayId.sql b/src/Sql/dbo/Stored Procedures/PlayItem_ReadByPlayId.sql new file mode 100644 index 0000000000..29592c527b --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/PlayItem_ReadByPlayId.sql @@ -0,0 +1,17 @@ +CREATE PROCEDURE [dbo].[PlayItem_ReadByPlayId] + @PlayId NVARCHAR(256) +AS +BEGIN + SET NOCOUNT ON + + SELECT + [Id], + [PlayId], + [UserId], + [OrganizationId], + [CreationDate] + FROM + [dbo].[PlayItem] + WHERE + [PlayId] = @PlayId +END diff --git a/src/Sql/dbo/Tables/PlayItem.sql b/src/Sql/dbo/Tables/PlayItem.sql new file mode 100644 index 0000000000..fbf3d2e0dd --- /dev/null +++ b/src/Sql/dbo/Tables/PlayItem.sql @@ -0,0 +1,23 @@ +CREATE TABLE [dbo].[PlayItem] ( + [Id] UNIQUEIDENTIFIER NOT NULL, + [PlayId] NVARCHAR (256) NOT NULL, + [UserId] UNIQUEIDENTIFIER NULL, + [OrganizationId] UNIQUEIDENTIFIER NULL, + [CreationDate] DATETIME2 (7) NOT NULL, + CONSTRAINT [PK_PlayItem] PRIMARY KEY CLUSTERED ([Id] ASC), + CONSTRAINT [FK_PlayItem_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id]) ON DELETE CASCADE, + CONSTRAINT [FK_PlayItem_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]) ON DELETE CASCADE, + CONSTRAINT [CK_PlayItem_UserOrOrganization] CHECK (([UserId] IS NOT NULL AND [OrganizationId] IS NULL) OR ([UserId] IS NULL AND [OrganizationId] IS NOT NULL)) +); + +GO +CREATE NONCLUSTERED INDEX [IX_PlayItem_PlayId] + ON [dbo].[PlayItem]([PlayId] ASC); + +GO +CREATE NONCLUSTERED INDEX [IX_PlayItem_UserId] + ON [dbo].[PlayItem]([UserId] ASC); + +GO +CREATE NONCLUSTERED INDEX [IX_PlayItem_OrganizationId] + ON [dbo].[PlayItem]([OrganizationId] ASC); diff --git a/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_Archive.sql b/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_Archive.sql index e55d94c0c4..605936043c 100644 --- a/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_Archive.sql +++ b/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_Archive.sql @@ -13,14 +13,13 @@ BEGIN INSERT INTO #Temp SELECT - [Id], - [UserId] + ucd.[Id], + ucd.[UserId] FROM - [dbo].[UserCipherDetails](@UserId) + [dbo].[UserCipherDetails](@UserId) ucd + INNER JOIN @Ids ids ON ids.Id = ucd.[Id] WHERE - [Edit] = 1 - AND [ArchivedDate] IS NULL - AND [Id] IN (SELECT * FROM @Ids) + ucd.[ArchivedDate] IS NULL DECLARE @UtcNow DATETIME2(7) = SYSUTCDATETIME(); UPDATE @@ -32,8 +31,9 @@ BEGIN CONVERT(NVARCHAR(30), @UtcNow, 127) ), [RevisionDate] = @UtcNow - WHERE - [Id] IN (SELECT [Id] FROM #Temp) + FROM [dbo].[Cipher] AS c + INNER JOIN #Temp AS t + ON t.[Id] = c.[Id]; EXEC [dbo].[User_BumpAccountRevisionDate] @UserId diff --git a/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_Unarchive.sql b/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_Unarchive.sql index 356e339ec9..f342adf2ac 100644 --- a/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_Unarchive.sql +++ b/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_Unarchive.sql @@ -13,14 +13,13 @@ BEGIN INSERT INTO #Temp SELECT - [Id], - [UserId] + ucd.[Id], + ucd.[UserId] FROM - [dbo].[UserCipherDetails](@UserId) + [dbo].[UserCipherDetails](@UserId) ucd + INNER JOIN @Ids ids ON ids.Id = ucd.[Id] WHERE - [Edit] = 1 - AND [ArchivedDate] IS NOT NULL - AND [Id] IN (SELECT * FROM @Ids) + ucd.[ArchivedDate] IS NOT NULL DECLARE @UtcNow DATETIME2(7) = SYSUTCDATETIME(); UPDATE @@ -32,8 +31,9 @@ BEGIN NULL ), [RevisionDate] = @UtcNow - WHERE - [Id] IN (SELECT [Id] FROM #Temp) + FROM [dbo].[Cipher] AS c + INNER JOIN #Temp AS t + ON t.[Id] = c.[Id]; EXEC [dbo].[User_BumpAccountRevisionDate] @UserId diff --git a/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerBulkRevokeTests.cs b/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerBulkRevokeTests.cs index 6645f29eae..3ea7d9ff5a 100644 --- a/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerBulkRevokeTests.cs +++ b/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerBulkRevokeTests.cs @@ -4,7 +4,6 @@ using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Api.IntegrationTest.Factories; using Bit.Api.IntegrationTest.Helpers; using Bit.Api.Models.Response; -using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; @@ -14,8 +13,6 @@ using Bit.Core.Billing.Enums; using Bit.Core.Enums; using Bit.Core.Models.Data; using Bit.Core.Repositories; -using Bit.Core.Services; -using NSubstitute; using Xunit; namespace Bit.Api.IntegrationTest.AdminConsole.Controllers; @@ -32,12 +29,6 @@ public class OrganizationUserControllerBulkRevokeTests : IClassFixture(featureService => - { - featureService - .IsEnabled(FeatureFlagKeys.BulkRevokeUsersV2) - .Returns(true); - }); _client = _factory.CreateClient(); _loginHelper = new LoginHelper(_factory, _client); } diff --git a/test/Api.IntegrationTest/Api.IntegrationTest.csproj b/test/Api.IntegrationTest/Api.IntegrationTest.csproj index a9d7fd502e..153803ef21 100644 --- a/test/Api.IntegrationTest/Api.IntegrationTest.csproj +++ b/test/Api.IntegrationTest/Api.IntegrationTest.csproj @@ -1,6 +1,8 @@ false + + $(WarningsNotAsErrors);CA1304;CA1305 diff --git a/test/Api.Test/Api.Test.csproj b/test/Api.Test/Api.Test.csproj index fb75246d4f..da9cdcff06 100644 --- a/test/Api.Test/Api.Test.csproj +++ b/test/Api.Test/Api.Test.csproj @@ -2,6 +2,8 @@ false + + $(WarningsNotAsErrors);CA1304 diff --git a/test/Api.Test/Tools/Controllers/SendsControllerTests.cs b/test/Api.Test/Tools/Controllers/SendsControllerTests.cs index 541e8a4903..e3a9ba4435 100644 --- a/test/Api.Test/Tools/Controllers/SendsControllerTests.cs +++ b/test/Api.Test/Tools/Controllers/SendsControllerTests.cs @@ -8,8 +8,8 @@ using Bit.Api.Tools.Models.Request; using Bit.Api.Tools.Models.Response; using Bit.Core.Entities; using Bit.Core.Exceptions; +using Bit.Core.Platform.Push; using Bit.Core.Services; -using Bit.Core.Settings; using Bit.Core.Tools.Entities; using Bit.Core.Tools.Enums; using Bit.Core.Tools.Models.Data; @@ -28,7 +28,6 @@ namespace Bit.Api.Test.Tools.Controllers; public class SendsControllerTests : IDisposable { private readonly SendsController _sut; - private readonly GlobalSettings _globalSettings; private readonly IUserService _userService; private readonly ISendRepository _sendRepository; private readonly INonAnonymousSendCommand _nonAnonymousSendCommand; @@ -37,6 +36,8 @@ public class SendsControllerTests : IDisposable private readonly ISendAuthorizationService _sendAuthorizationService; private readonly ISendFileStorageService _sendFileStorageService; private readonly ILogger _logger; + private readonly IFeatureService _featureService; + private readonly IPushNotificationService _pushNotificationService; public SendsControllerTests() { @@ -47,8 +48,9 @@ public class SendsControllerTests : IDisposable _sendOwnerQuery = Substitute.For(); _sendAuthorizationService = Substitute.For(); _sendFileStorageService = Substitute.For(); - _globalSettings = new GlobalSettings(); _logger = Substitute.For>(); + _featureService = Substitute.For(); + _pushNotificationService = Substitute.For(); _sut = new SendsController( _sendRepository, @@ -59,7 +61,8 @@ public class SendsControllerTests : IDisposable _sendOwnerQuery, _sendFileStorageService, _logger, - _globalSettings + _featureService, + _pushNotificationService ); } @@ -96,8 +99,8 @@ public class SendsControllerTests : IDisposable { var now = DateTime.UtcNow; var expected = "You cannot have a Send with a deletion date that far " + - "into the future. Adjust the Deletion Date to a value less than 31 days from now " + - "and try again."; + "into the future. Adjust the Deletion Date to a value less than 31 days from now " + + "and try again."; var request = new SendRequestModel() { DeletionDate = now.AddDays(32) }; var exception = await Assert.ThrowsAsync(() => _sut.Post(request)); @@ -109,9 +112,10 @@ public class SendsControllerTests : IDisposable { var now = DateTime.UtcNow; var expected = "You cannot have a Send with a deletion date that far " + - "into the future. Adjust the Deletion Date to a value less than 31 days from now " + - "and try again."; - var request = new SendRequestModel() { Type = SendType.File, FileLength = 1024L, DeletionDate = now.AddDays(32) }; + "into the future. Adjust the Deletion Date to a value less than 31 days from now " + + "and try again."; + var request = + new SendRequestModel() { Type = SendType.File, FileLength = 1024L, DeletionDate = now.AddDays(32) }; var exception = await Assert.ThrowsAsync(() => _sut.PostFile(request)); Assert.Equal(expected, exception.Message); @@ -409,7 +413,8 @@ public class SendsControllerTests : IDisposable } [Theory, AutoData] - public async Task PutRemovePassword_WithWrongUser_ThrowsNotFoundException(Guid userId, Guid otherUserId, Guid sendId) + public async Task PutRemovePassword_WithWrongUser_ThrowsNotFoundException(Guid userId, Guid otherUserId, + Guid sendId) { _userService.GetProperUserId(Arg.Any()).Returns(userId); var existingSend = new Send @@ -753,4 +758,683 @@ public class SendsControllerTests : IDisposable s.Password == null && s.Emails == null)); } + + #region Authenticated Access Endpoints + + [Theory, AutoData] + public async Task AccessUsingAuth_WithValidSend_ReturnsSendAccessResponse(Guid sendId, User creator) + { + var send = new Send + { + Id = sendId, + UserId = creator.Id, + Type = SendType.Text, + Data = JsonSerializer.Serialize(new SendTextData("Test", "Notes", "Text", false)), + HideEmail = false, + DeletionDate = DateTime.UtcNow.AddDays(7), + ExpirationDate = null, + Disabled = false, + AccessCount = 0, + MaxAccessCount = null + }; + var user = CreateUserWithSendIdClaim(sendId); + _sut.ControllerContext = CreateControllerContextWithUser(user); + _sendRepository.GetByIdAsync(sendId).Returns(send); + _userService.GetUserByIdAsync(creator.Id).Returns(creator); + + var result = await _sut.AccessUsingAuth(); + + Assert.NotNull(result); + var objectResult = Assert.IsType(result); + var response = Assert.IsType(objectResult.Value); + Assert.Equal(CoreHelpers.Base64UrlEncode(sendId.ToByteArray()), response.Id); + Assert.Equal(creator.Email, response.CreatorIdentifier); + await _sendRepository.Received(1).GetByIdAsync(sendId); + await _userService.Received(1).GetUserByIdAsync(creator.Id); + } + + [Theory, AutoData] + public async Task AccessUsingAuth_WithHideEmail_DoesNotIncludeCreatorIdentifier(Guid sendId, User creator) + { + var send = new Send + { + Id = sendId, + UserId = creator.Id, + Type = SendType.Text, + Data = JsonSerializer.Serialize(new SendTextData("Test", "Notes", "Text", false)), + HideEmail = true, + DeletionDate = DateTime.UtcNow.AddDays(7), + ExpirationDate = null, + Disabled = false, + AccessCount = 0, + MaxAccessCount = null + }; + var user = CreateUserWithSendIdClaim(sendId); + _sut.ControllerContext = CreateControllerContextWithUser(user); + _sendRepository.GetByIdAsync(sendId).Returns(send); + + var result = await _sut.AccessUsingAuth(); + + Assert.NotNull(result); + var objectResult = Assert.IsType(result); + var response = Assert.IsType(objectResult.Value); + Assert.Equal(CoreHelpers.Base64UrlEncode(sendId.ToByteArray()), response.Id); + Assert.Null(response.CreatorIdentifier); + await _sendRepository.Received(1).GetByIdAsync(sendId); + await _userService.DidNotReceive().GetUserByIdAsync(Arg.Any()); + } + + [Theory, AutoData] + public async Task AccessUsingAuth_WithNoUserId_DoesNotIncludeCreatorIdentifier(Guid sendId) + { + var send = new Send + { + Id = sendId, + UserId = null, + Type = SendType.Text, + Data = JsonSerializer.Serialize(new SendTextData("Test", "Notes", "Text", false)), + HideEmail = false, + DeletionDate = DateTime.UtcNow.AddDays(7), + ExpirationDate = null, + Disabled = false, + AccessCount = 0, + MaxAccessCount = null + }; + var user = CreateUserWithSendIdClaim(sendId); + _sut.ControllerContext = CreateControllerContextWithUser(user); + _sendRepository.GetByIdAsync(sendId).Returns(send); + + var result = await _sut.AccessUsingAuth(); + + Assert.NotNull(result); + var objectResult = Assert.IsType(result); + var response = Assert.IsType(objectResult.Value); + Assert.Equal(CoreHelpers.Base64UrlEncode(sendId.ToByteArray()), response.Id); + Assert.Null(response.CreatorIdentifier); + await _sendRepository.Received(1).GetByIdAsync(sendId); + await _userService.DidNotReceive().GetUserByIdAsync(Arg.Any()); + } + + [Theory, AutoData] + public async Task AccessUsingAuth_WithNonExistentSend_ThrowsBadRequestException(Guid sendId) + { + var user = CreateUserWithSendIdClaim(sendId); + _sut.ControllerContext = CreateControllerContextWithUser(user); + _sendRepository.GetByIdAsync(sendId).Returns((Send)null); + + var exception = + await Assert.ThrowsAsync(() => _sut.AccessUsingAuth()); + + Assert.Equal("Could not locate send", exception.Message); + await _sendRepository.Received(1).GetByIdAsync(sendId); + } + + [Theory, AutoData] + public async Task AccessUsingAuth_WithFileSend_ReturnsCorrectResponse(Guid sendId, User creator) + { + var fileData = new SendFileData("Test File", "Notes", "document.pdf") { Id = "file-123", Size = 2048 }; + var send = new Send + { + Id = sendId, + UserId = creator.Id, + Type = SendType.File, + Data = JsonSerializer.Serialize(fileData), + HideEmail = false, + DeletionDate = DateTime.UtcNow.AddDays(7), + ExpirationDate = null, + Disabled = false, + AccessCount = 0, + MaxAccessCount = null + }; + var user = CreateUserWithSendIdClaim(sendId); + _sut.ControllerContext = CreateControllerContextWithUser(user); + _sendRepository.GetByIdAsync(sendId).Returns(send); + _userService.GetUserByIdAsync(creator.Id).Returns(creator); + + var result = await _sut.AccessUsingAuth(); + + Assert.NotNull(result); + var objectResult = Assert.IsType(result); + var response = Assert.IsType(objectResult.Value); + Assert.Equal(CoreHelpers.Base64UrlEncode(sendId.ToByteArray()), response.Id); + Assert.Equal(SendType.File, response.Type); + Assert.NotNull(response.File); + Assert.Equal("file-123", response.File.Id); + Assert.Equal(creator.Email, response.CreatorIdentifier); + } + + [Theory, AutoData] + public async Task GetSendFileDownloadDataUsingAuth_WithValidFileId_ReturnsDownloadUrl( + Guid sendId, string fileId, string expectedUrl) + { + var fileData = new SendFileData("Test File", "Notes", "document.pdf") { Id = fileId, Size = 2048 }; + var send = new Send + { + Id = sendId, + Type = SendType.File, + Data = JsonSerializer.Serialize(fileData), + DeletionDate = DateTime.UtcNow.AddDays(7), + ExpirationDate = null, + Disabled = false, + AccessCount = 0, + MaxAccessCount = null + }; + var user = CreateUserWithSendIdClaim(sendId); + _sut.ControllerContext = CreateControllerContextWithUser(user); + _sendRepository.GetByIdAsync(sendId).Returns(send); + _sendFileStorageService.GetSendFileDownloadUrlAsync(send, fileId).Returns(expectedUrl); + + var result = await _sut.GetSendFileDownloadDataUsingAuth(fileId); + + Assert.NotNull(result); + var objectResult = Assert.IsType(result); + var response = Assert.IsType(objectResult.Value); + Assert.Equal(fileId, response.Id); + Assert.Equal(expectedUrl, response.Url); + await _sendRepository.Received(1).GetByIdAsync(sendId); + await _sendFileStorageService.Received(1).GetSendFileDownloadUrlAsync(send, fileId); + } + + [Theory, AutoData] + public async Task GetSendFileDownloadDataUsingAuth_WithNonExistentSend_ThrowsBadRequestException( + Guid sendId, string fileId) + { + var user = CreateUserWithSendIdClaim(sendId); + _sut.ControllerContext = CreateControllerContextWithUser(user); + _sendRepository.GetByIdAsync(sendId).Returns((Send)null); + + var exception = + await Assert.ThrowsAsync(() => _sut.GetSendFileDownloadDataUsingAuth(fileId)); + + Assert.Equal("Could not locate send", exception.Message); + await _sendRepository.Received(1).GetByIdAsync(sendId); + await _sendFileStorageService.DidNotReceive() + .GetSendFileDownloadUrlAsync(Arg.Any(), Arg.Any()); + } + + [Theory, AutoData] + public async Task GetSendFileDownloadDataUsingAuth_WithTextSend_StillReturnsResponse( + Guid sendId, string fileId, string expectedUrl) + { + var send = new Send + { + Id = sendId, + Type = SendType.Text, + Data = JsonSerializer.Serialize(new SendTextData("Test", "Notes", "Text", false)), + DeletionDate = DateTime.UtcNow.AddDays(7), + ExpirationDate = null, + Disabled = false, + AccessCount = 0, + MaxAccessCount = null + }; + var user = CreateUserWithSendIdClaim(sendId); + _sut.ControllerContext = CreateControllerContextWithUser(user); + _sendRepository.GetByIdAsync(sendId).Returns(send); + _sendFileStorageService.GetSendFileDownloadUrlAsync(send, fileId).Returns(expectedUrl); + + var result = await _sut.GetSendFileDownloadDataUsingAuth(fileId); + + Assert.NotNull(result); + var objectResult = Assert.IsType(result); + var response = Assert.IsType(objectResult.Value); + Assert.Equal(fileId, response.Id); + Assert.Equal(expectedUrl, response.Url); + } + + #region AccessUsingAuth Validation Tests + + [Theory, AutoData] + public async Task AccessUsingAuth_WithExpiredSend_ThrowsNotFoundException(Guid sendId) + { + var send = new Send + { + Id = sendId, + UserId = Guid.NewGuid(), + Type = SendType.Text, + Data = JsonSerializer.Serialize(new SendTextData("Test", "Notes", "Text", false)), + DeletionDate = DateTime.UtcNow.AddDays(7), + ExpirationDate = DateTime.UtcNow.AddDays(-1), // Expired yesterday + Disabled = false, + AccessCount = 0, + MaxAccessCount = null + }; + var user = CreateUserWithSendIdClaim(sendId); + _sut.ControllerContext = CreateControllerContextWithUser(user); + _sendRepository.GetByIdAsync(sendId).Returns(send); + + await Assert.ThrowsAsync(() => _sut.AccessUsingAuth()); + + await _sendRepository.Received(1).GetByIdAsync(sendId); + } + + [Theory, AutoData] + public async Task AccessUsingAuth_WithDeletedSend_ThrowsNotFoundException(Guid sendId) + { + var send = new Send + { + Id = sendId, + UserId = Guid.NewGuid(), + Type = SendType.Text, + Data = JsonSerializer.Serialize(new SendTextData("Test", "Notes", "Text", false)), + DeletionDate = DateTime.UtcNow.AddDays(-1), // Should have been deleted yesterday + ExpirationDate = null, + Disabled = false, + AccessCount = 0, + MaxAccessCount = null + }; + var user = CreateUserWithSendIdClaim(sendId); + _sut.ControllerContext = CreateControllerContextWithUser(user); + _sendRepository.GetByIdAsync(sendId).Returns(send); + + await Assert.ThrowsAsync(() => _sut.AccessUsingAuth()); + + await _sendRepository.Received(1).GetByIdAsync(sendId); + } + + [Theory, AutoData] + public async Task AccessUsingAuth_WithDisabledSend_ThrowsNotFoundException(Guid sendId) + { + var send = new Send + { + Id = sendId, + UserId = Guid.NewGuid(), + Type = SendType.Text, + Data = JsonSerializer.Serialize(new SendTextData("Test", "Notes", "Text", false)), + DeletionDate = DateTime.UtcNow.AddDays(7), + ExpirationDate = null, + Disabled = true, // Disabled + AccessCount = 0, + MaxAccessCount = null + }; + var user = CreateUserWithSendIdClaim(sendId); + _sut.ControllerContext = CreateControllerContextWithUser(user); + _sendRepository.GetByIdAsync(sendId).Returns(send); + + await Assert.ThrowsAsync(() => _sut.AccessUsingAuth()); + + await _sendRepository.Received(1).GetByIdAsync(sendId); + } + + [Theory, AutoData] + public async Task AccessUsingAuth_WithAccessCountExceeded_ThrowsNotFoundException(Guid sendId) + { + var send = new Send + { + Id = sendId, + UserId = Guid.NewGuid(), + Type = SendType.Text, + Data = JsonSerializer.Serialize(new SendTextData("Test", "Notes", "Text", false)), + DeletionDate = DateTime.UtcNow.AddDays(7), + ExpirationDate = null, + Disabled = false, + AccessCount = 5, + MaxAccessCount = 5 // Limit reached + }; + var user = CreateUserWithSendIdClaim(sendId); + _sut.ControllerContext = CreateControllerContextWithUser(user); + _sendRepository.GetByIdAsync(sendId).Returns(send); + + await Assert.ThrowsAsync(() => _sut.AccessUsingAuth()); + + await _sendRepository.Received(1).GetByIdAsync(sendId); + } + + #endregion + + #region GetSendFileDownloadDataUsingAuth Validation Tests + + [Theory, AutoData] + public async Task GetSendFileDownloadDataUsingAuth_WithExpiredSend_ThrowsNotFoundException( + Guid sendId, string fileId) + { + var send = new Send + { + Id = sendId, + Type = SendType.File, + Data = JsonSerializer.Serialize(new SendFileData("Test", "Notes", "file.pdf")), + DeletionDate = DateTime.UtcNow.AddDays(7), + ExpirationDate = DateTime.UtcNow.AddDays(-1), // Expired + Disabled = false, + AccessCount = 0, + MaxAccessCount = null + }; + var user = CreateUserWithSendIdClaim(sendId); + _sut.ControllerContext = CreateControllerContextWithUser(user); + _sendRepository.GetByIdAsync(sendId).Returns(send); + + await Assert.ThrowsAsync(() => _sut.GetSendFileDownloadDataUsingAuth(fileId)); + + await _sendRepository.Received(1).GetByIdAsync(sendId); + } + + [Theory, AutoData] + public async Task GetSendFileDownloadDataUsingAuth_WithDeletedSend_ThrowsNotFoundException( + Guid sendId, string fileId) + { + var send = new Send + { + Id = sendId, + Type = SendType.File, + Data = JsonSerializer.Serialize(new SendFileData("Test", "Notes", "file.pdf")), + DeletionDate = DateTime.UtcNow.AddDays(-1), // Deleted + ExpirationDate = null, + Disabled = false, + AccessCount = 0, + MaxAccessCount = null + }; + var user = CreateUserWithSendIdClaim(sendId); + _sut.ControllerContext = CreateControllerContextWithUser(user); + _sendRepository.GetByIdAsync(sendId).Returns(send); + + await Assert.ThrowsAsync(() => _sut.GetSendFileDownloadDataUsingAuth(fileId)); + + await _sendRepository.Received(1).GetByIdAsync(sendId); + } + + [Theory, AutoData] + public async Task GetSendFileDownloadDataUsingAuth_WithDisabledSend_ThrowsNotFoundException( + Guid sendId, string fileId) + { + var send = new Send + { + Id = sendId, + Type = SendType.File, + Data = JsonSerializer.Serialize(new SendFileData("Test", "Notes", "file.pdf")), + DeletionDate = DateTime.UtcNow.AddDays(7), + ExpirationDate = null, + Disabled = true, // Disabled + AccessCount = 0, + MaxAccessCount = null + }; + var user = CreateUserWithSendIdClaim(sendId); + _sut.ControllerContext = CreateControllerContextWithUser(user); + _sendRepository.GetByIdAsync(sendId).Returns(send); + + await Assert.ThrowsAsync(() => _sut.GetSendFileDownloadDataUsingAuth(fileId)); + + await _sendRepository.Received(1).GetByIdAsync(sendId); + } + + [Theory, AutoData] + public async Task GetSendFileDownloadDataUsingAuth_WithAccessCountExceeded_ThrowsNotFoundException( + Guid sendId, string fileId) + { + var send = new Send + { + Id = sendId, + Type = SendType.File, + Data = JsonSerializer.Serialize(new SendFileData("Test", "Notes", "file.pdf")), + DeletionDate = DateTime.UtcNow.AddDays(7), + ExpirationDate = null, + Disabled = false, + AccessCount = 10, + MaxAccessCount = 10 // Limit reached + }; + var user = CreateUserWithSendIdClaim(sendId); + _sut.ControllerContext = CreateControllerContextWithUser(user); + _sendRepository.GetByIdAsync(sendId).Returns(send); + + await Assert.ThrowsAsync(() => _sut.GetSendFileDownloadDataUsingAuth(fileId)); + + await _sendRepository.Received(1).GetByIdAsync(sendId); + } + + #endregion + + #endregion + + #region PutRemoveAuth Tests + + [Theory, AutoData] + public async Task PutRemoveAuth_WithPasswordProtectedSend_RemovesPasswordAndSetsAuthTypeNone(Guid userId, + Guid sendId) + { + _userService.GetProperUserId(Arg.Any()).Returns(userId); + var existingSend = new Send + { + Id = sendId, + UserId = userId, + Type = SendType.Text, + Data = JsonSerializer.Serialize(new SendTextData("Test", "Notes", "Text", false)), + Password = "hashed-password", + Emails = null, + AuthType = AuthType.Password + }; + _sendRepository.GetByIdAsync(sendId).Returns(existingSend); + + var result = await _sut.PutRemoveAuth(sendId.ToString()); + + Assert.NotNull(result); + Assert.Equal(sendId, result.Id); + Assert.Equal(AuthType.None, result.AuthType); + Assert.Null(result.Password); + Assert.Null(result.Emails); + await _nonAnonymousSendCommand.Received(1).SaveSendAsync(Arg.Is(s => + s.Id == sendId && + s.Password == null && + s.Emails == null && + s.AuthType == AuthType.None)); + } + + [Theory, AutoData] + public async Task PutRemoveAuth_WithEmailProtectedSend_RemovesEmailsAndSetsAuthTypeNone(Guid userId, Guid sendId) + { + _userService.GetProperUserId(Arg.Any()).Returns(userId); + var existingSend = new Send + { + Id = sendId, + UserId = userId, + Type = SendType.Text, + Data = JsonSerializer.Serialize(new SendTextData("Test", "Notes", "Text", false)), + Password = null, + Emails = "test@example.com,user@example.com", + AuthType = AuthType.Email + }; + _sendRepository.GetByIdAsync(sendId).Returns(existingSend); + + var result = await _sut.PutRemoveAuth(sendId.ToString()); + + Assert.NotNull(result); + Assert.Equal(sendId, result.Id); + Assert.Equal(AuthType.None, result.AuthType); + Assert.Null(result.Password); + Assert.Null(result.Emails); + await _nonAnonymousSendCommand.Received(1).SaveSendAsync(Arg.Is(s => + s.Id == sendId && + s.Password == null && + s.Emails == null && + s.AuthType == AuthType.None)); + } + + [Theory, AutoData] + public async Task PutRemoveAuth_WithSendAlreadyHavingNoAuth_StillSucceeds(Guid userId, Guid sendId) + { + _userService.GetProperUserId(Arg.Any()).Returns(userId); + var existingSend = new Send + { + Id = sendId, + UserId = userId, + Type = SendType.Text, + Data = JsonSerializer.Serialize(new SendTextData("Test", "Notes", "Text", false)), + Password = null, + Emails = null, + AuthType = AuthType.None + }; + _sendRepository.GetByIdAsync(sendId).Returns(existingSend); + + var result = await _sut.PutRemoveAuth(sendId.ToString()); + + Assert.NotNull(result); + Assert.Equal(sendId, result.Id); + Assert.Equal(AuthType.None, result.AuthType); + Assert.Null(result.Password); + Assert.Null(result.Emails); + await _nonAnonymousSendCommand.Received(1).SaveSendAsync(Arg.Is(s => + s.Id == sendId && + s.Password == null && + s.Emails == null && + s.AuthType == AuthType.None)); + } + + [Theory, AutoData] + public async Task PutRemoveAuth_WithFileSend_RemovesAuthAndPreservesFileData(Guid userId, Guid sendId) + { + _userService.GetProperUserId(Arg.Any()).Returns(userId); + var fileData = new SendFileData("Test File", "Notes", "document.pdf") { Id = "file-123", Size = 2048 }; + var existingSend = new Send + { + Id = sendId, + UserId = userId, + Type = SendType.File, + Data = JsonSerializer.Serialize(fileData), + Password = "hashed-password", + Emails = null, + AuthType = AuthType.Password + }; + _sendRepository.GetByIdAsync(sendId).Returns(existingSend); + + var result = await _sut.PutRemoveAuth(sendId.ToString()); + + Assert.NotNull(result); + Assert.Equal(sendId, result.Id); + Assert.Equal(AuthType.None, result.AuthType); + Assert.Equal(SendType.File, result.Type); + Assert.NotNull(result.File); + Assert.Equal("file-123", result.File.Id); + Assert.Null(result.Password); + Assert.Null(result.Emails); + } + + [Theory, AutoData] + public async Task PutRemoveAuth_WithNonExistentSend_ThrowsNotFoundException(Guid userId, Guid sendId) + { + _userService.GetProperUserId(Arg.Any()).Returns(userId); + _sendRepository.GetByIdAsync(sendId).Returns((Send)null); + + await Assert.ThrowsAsync(() => _sut.PutRemoveAuth(sendId.ToString())); + + await _sendRepository.Received(1).GetByIdAsync(sendId); + await _nonAnonymousSendCommand.DidNotReceive().SaveSendAsync(Arg.Any()); + } + + [Theory, AutoData] + public async Task PutRemoveAuth_WithWrongUser_ThrowsNotFoundException(Guid userId, Guid otherUserId, Guid sendId) + { + _userService.GetProperUserId(Arg.Any()).Returns(userId); + var existingSend = new Send + { + Id = sendId, + UserId = otherUserId, + Type = SendType.Text, + Data = JsonSerializer.Serialize(new SendTextData("Test", "Notes", "Text", false)), + Password = "hashed-password", + AuthType = AuthType.Password + }; + _sendRepository.GetByIdAsync(sendId).Returns(existingSend); + + await Assert.ThrowsAsync(() => _sut.PutRemoveAuth(sendId.ToString())); + + await _sendRepository.Received(1).GetByIdAsync(sendId); + await _nonAnonymousSendCommand.DidNotReceive().SaveSendAsync(Arg.Any()); + } + + [Theory, AutoData] + public async Task PutRemoveAuth_WithNullUserId_ThrowsInvalidOperationException(Guid sendId) + { + _userService.GetProperUserId(Arg.Any()).Returns((Guid?)null); + + var exception = + await Assert.ThrowsAsync(() => _sut.PutRemoveAuth(sendId.ToString())); + + Assert.Equal("User ID not found", exception.Message); + await _sendRepository.DidNotReceive().GetByIdAsync(Arg.Any()); + await _nonAnonymousSendCommand.DidNotReceive().SaveSendAsync(Arg.Any()); + } + + [Theory, AutoData] + public async Task PutRemoveAuth_WithSendHavingBothPasswordAndEmails_RemovesBoth(Guid userId, Guid sendId) + { + _userService.GetProperUserId(Arg.Any()).Returns(userId); + var existingSend = new Send + { + Id = sendId, + UserId = userId, + Type = SendType.Text, + Data = JsonSerializer.Serialize(new SendTextData("Test", "Notes", "Text", false)), + Password = "hashed-password", + Emails = "test@example.com", + AuthType = AuthType.Password + }; + _sendRepository.GetByIdAsync(sendId).Returns(existingSend); + + var result = await _sut.PutRemoveAuth(sendId.ToString()); + + Assert.NotNull(result); + Assert.Equal(sendId, result.Id); + Assert.Equal(AuthType.None, result.AuthType); + Assert.Null(result.Password); + Assert.Null(result.Emails); + await _nonAnonymousSendCommand.Received(1).SaveSendAsync(Arg.Is(s => + s.Id == sendId && + s.Password == null && + s.Emails == null && + s.AuthType == AuthType.None)); + } + + [Theory, AutoData] + public async Task PutRemoveAuth_PreservesOtherSendProperties(Guid userId, Guid sendId) + { + _userService.GetProperUserId(Arg.Any()).Returns(userId); + var deletionDate = DateTime.UtcNow.AddDays(7); + var expirationDate = DateTime.UtcNow.AddDays(3); + var existingSend = new Send + { + Id = sendId, + UserId = userId, + Type = SendType.Text, + Data = JsonSerializer.Serialize(new SendTextData("Test", "Notes", "Text", false)), + Password = "hashed-password", + AuthType = AuthType.Password, + Key = "encryption-key", + MaxAccessCount = 10, + AccessCount = 3, + DeletionDate = deletionDate, + ExpirationDate = expirationDate, + Disabled = false, + HideEmail = true + }; + _sendRepository.GetByIdAsync(sendId).Returns(existingSend); + + var result = await _sut.PutRemoveAuth(sendId.ToString()); + + Assert.NotNull(result); + Assert.Equal(sendId, result.Id); + Assert.Equal(AuthType.None, result.AuthType); + // Verify other properties are preserved + Assert.Equal("encryption-key", result.Key); + Assert.Equal(10, result.MaxAccessCount); + Assert.Equal(3, result.AccessCount); + Assert.Equal(deletionDate, result.DeletionDate); + Assert.Equal(expirationDate, result.ExpirationDate); + Assert.False(result.Disabled); + Assert.True(result.HideEmail); + } + + #endregion + + #region Test Helpers + + private static ClaimsPrincipal CreateUserWithSendIdClaim(Guid sendId) + { + var claims = new List { new Claim("send_id", sendId.ToString()) }; + var identity = new ClaimsIdentity(claims, "TestAuth"); + return new ClaimsPrincipal(identity); + } + + private static ControllerContext CreateControllerContextWithUser(ClaimsPrincipal user) + { + return new ControllerContext { HttpContext = new Microsoft.AspNetCore.Http.DefaultHttpContext { User = user } }; + } + + #endregion } diff --git a/test/Common/AutoFixture/SutProvider.cs b/test/Common/AutoFixture/SutProvider.cs index e1b37a9827..295f6bc950 100644 --- a/test/Common/AutoFixture/SutProvider.cs +++ b/test/Common/AutoFixture/SutProvider.cs @@ -26,6 +26,7 @@ public class SutProvider : ISutProvider public TSut Sut { get; private set; } public Type SutType => typeof(TSut); + public IFixture Fixture => _fixture; public SutProvider() : this(new Fixture()) { } @@ -65,6 +66,19 @@ public class SutProvider : ISutProvider return this; } + /// + /// Creates and registers a dependency to be injected when the sut is created. + /// + /// The Dependency type to create + /// The (optional) parameter name to register the dependency under + /// The created dependency value + public TDep CreateDependency(string parameterName = "") + { + var dependency = _fixture.Create(); + SetDependency(dependency, parameterName); + return dependency; + } + /// /// Gets a dependency of the sut. Can only be called after the dependency has been set, either explicitly with /// or automatically with . diff --git a/test/Common/Common.csproj b/test/Common/Common.csproj index 2f11798cef..3d1b6a6c3b 100644 --- a/test/Common/Common.csproj +++ b/test/Common/Common.csproj @@ -2,6 +2,8 @@ false Bit.Test.Common + + $(WarningsNotAsErrors);CA1305 diff --git a/test/Core.Test/AdminConsole/Entities/OrganizationTests.cs b/test/Core.Test/AdminConsole/Entities/OrganizationTests.cs index 7fcda324d9..aa0c2c80c3 100644 --- a/test/Core.Test/AdminConsole/Entities/OrganizationTests.cs +++ b/test/Core.Test/AdminConsole/Entities/OrganizationTests.cs @@ -1,7 +1,10 @@ -using System.Text.Json; +using System.Reflection; +using System.Text.Json; +using System.Text.RegularExpressions; using Bit.Core.AdminConsole.Entities; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models; +using Bit.Core.Billing.Organizations.Models; using Bit.Test.Common.Helpers; using Xunit; @@ -115,4 +118,105 @@ public class OrganizationTests Assert.True(organization.UseDisableSmAdsForUsers); } + + [Fact] + public void UpdateFromLicense_AppliesAllLicenseProperties() + { + // This test ensures that when a new property is added to OrganizationLicense, + // it is also applied to the Organization in UpdateFromLicense(). + // This is the fourth step in the license synchronization pipeline: + // Property → Constant → Claim → Extraction → Application + + // 1. Get all public properties from OrganizationLicense + var licenseProperties = typeof(OrganizationLicense) + .GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Select(p => p.Name) + .ToHashSet(); + + // 2. Define properties that don't need to be applied to Organization + var excludedProperties = new HashSet + { + // Internal/computed properties + "SignatureBytes", // Computed from Signature property + "ValidLicenseVersion", // Internal property, not serialized + "CurrentLicenseFileVersion", // Constant field, not an instance property + "Hash", // Signature-related, not applied to org + "Signature", // Signature-related, not applied to org + "Token", // The JWT itself, not applied to org + "Version", // License version, not stored on org + + // Properties intentionally excluded from UpdateFromLicense + "Id", // Self-hosted org has its own unique Guid + "MaxStorageGb", // Not enforced for self-hosted (per comment in UpdateFromLicense) + + // Properties not stored on Organization model + "LicenseType", // Not a property on Organization + "InstallationId", // Not a property on Organization + "Issued", // Not a property on Organization + "Refresh", // Not a property on Organization + "ExpirationWithoutGracePeriod", // Not a property on Organization + "Trial", // Not a property on Organization + "Expires", // Mapped to ExpirationDate on Organization (different name) + + // Deprecated properties not applied + "LimitCollectionCreationDeletion", // Deprecated, not applied + "AllowAdminAccessToAllCollectionItems", // Deprecated, not applied + }; + + // 3. Get properties that should be applied + var propertiesThatShouldBeApplied = licenseProperties + .Except(excludedProperties) + .ToHashSet(); + + // 4. Read Organization.UpdateFromLicense source code + var organizationSourcePath = Path.Combine( + Directory.GetCurrentDirectory(), + "..", "..", "..", "..", "..", "src", "Core", "AdminConsole", "Entities", "Organization.cs"); + var sourceCode = File.ReadAllText(organizationSourcePath); + + // 5. Find all property assignments in UpdateFromLicense method + // Pattern matches: PropertyName = license.PropertyName + // This regex looks for assignments like "Name = license.Name" or "ExpirationDate = license.Expires" + var assignmentPattern = @"(\w+)\s*=\s*license\.(\w+)"; + var matches = Regex.Matches(sourceCode, assignmentPattern); + + var appliedProperties = new HashSet(); + foreach (Match match in matches) + { + // Get the license property name (right side of assignment) + var licensePropertyName = match.Groups[2].Value; + appliedProperties.Add(licensePropertyName); + } + + // Special case: Expires is mapped to ExpirationDate + if (appliedProperties.Contains("Expires")) + { + appliedProperties.Add("Expires"); // Already added, but being explicit + } + + // 6. Find missing applications + var missingApplications = propertiesThatShouldBeApplied + .Except(appliedProperties) + .OrderBy(p => p) + .ToList(); + + // 7. Build error message with guidance + var errorMessage = ""; + if (missingApplications.Any()) + { + errorMessage = $"The following OrganizationLicense properties are NOT applied to Organization in UpdateFromLicense():\n"; + errorMessage += string.Join("\n", missingApplications.Select(p => $" - {p}")); + errorMessage += "\n\nPlease add the following lines to Organization.UpdateFromLicense():\n"; + foreach (var prop in missingApplications) + { + errorMessage += $" {prop} = license.{prop};\n"; + } + errorMessage += "\nNote: If the property maps to a different name on Organization (like Expires → ExpirationDate), adjust accordingly."; + } + + // 8. Assert - if this fails, the error message guides the developer to add the application + Assert.True( + !missingApplications.Any(), + $"\n{errorMessage}"); + } } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/AutomaticUserConfirmationPolicyEventHandlerTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/AutomaticUserConfirmationPolicyEventHandlerTests.cs index 3c9fd9a9e9..e2c9de4d6f 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/AutomaticUserConfirmationPolicyEventHandlerTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/AutomaticUserConfirmationPolicyEventHandlerTests.cs @@ -283,7 +283,7 @@ public class AutomaticUserConfirmationPolicyEventHandlerTests OrganizationId = policyUpdate.OrganizationId, Type = OrganizationUserType.User, Status = OrganizationUserStatusType.Invited, - UserId = Guid.NewGuid(), + UserId = null, Email = "invited@example.com" }; @@ -302,6 +302,56 @@ public class AutomaticUserConfirmationPolicyEventHandlerTests Assert.True(string.IsNullOrEmpty(result)); } + [Theory, BitAutoData] + public async Task ValidateAsync_EnablingPolicy_MixedUsersWithNullUserId_HandlesCorrectly( + [PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate, + Guid confirmedUserId, + SutProvider sutProvider) + { + // Arrange + var invitedUser = new OrganizationUserUserDetails + { + Id = Guid.NewGuid(), + OrganizationId = policyUpdate.OrganizationId, + Type = OrganizationUserType.User, + Status = OrganizationUserStatusType.Invited, + UserId = null, + Email = "invited@example.com" + }; + + var confirmedUser = new OrganizationUserUserDetails + { + Id = Guid.NewGuid(), + OrganizationId = policyUpdate.OrganizationId, + Type = OrganizationUserType.User, + Status = OrganizationUserStatusType.Confirmed, + UserId = confirmedUserId, + Email = "confirmed@example.com" + }; + + sutProvider.GetDependency() + .GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId) + .Returns([invitedUser, confirmedUser]); + + sutProvider.GetDependency() + .GetManyByManyUsersAsync(Arg.Any>()) + .Returns([]); + + sutProvider.GetDependency() + .GetManyByManyUsersAsync(Arg.Any>()) + .Returns([]); + + // Act + var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null); + + // Assert + Assert.True(string.IsNullOrEmpty(result)); + + await sutProvider.GetDependency() + .Received(1) + .GetManyByManyUsersAsync(Arg.Is>(ids => ids.Count() == 1 && ids.First() == confirmedUserId)); + } + [Theory, BitAutoData] public async Task ValidateAsync_EnablingPolicy_RevokedUsersIncluded_InComplianceCheck( [PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate, diff --git a/test/Core.Test/Billing/Licenses/LicenseConstantsTests.cs b/test/Core.Test/Billing/Licenses/LicenseConstantsTests.cs new file mode 100644 index 0000000000..df0e7133ad --- /dev/null +++ b/test/Core.Test/Billing/Licenses/LicenseConstantsTests.cs @@ -0,0 +1,68 @@ +using System.Reflection; +using Bit.Core.Billing.Licenses; +using Bit.Core.Billing.Organizations.Models; +using Xunit; + +namespace Bit.Core.Test.Billing.Licenses; + +public class LicenseConstantsTests +{ + [Fact] + public void OrganizationLicenseConstants_HasConstantForEveryLicenseProperty() + { + // This test ensures that when a new property is added to OrganizationLicense, + // a corresponding constant is added to OrganizationLicenseConstants. + // This is the first step in the license synchronization pipeline: + // Property → Constant → Claim → Extraction → Application + + // 1. Get all public properties from OrganizationLicense + var licenseProperties = typeof(OrganizationLicense) + .GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Select(p => p.Name) + .ToHashSet(); + + // 2. Get all constants from OrganizationLicenseConstants + var constants = typeof(OrganizationLicenseConstants) + .GetFields(BindingFlags.Public | BindingFlags.Static) + .Where(f => f.IsLiteral && !f.IsInitOnly) + .Select(f => f.GetValue(null) as string) + .ToHashSet(); + + // 3. Define properties that don't need constants (internal/computed/non-claims properties) + var excludedProperties = new HashSet + { + "SignatureBytes", // Computed from Signature property + "ValidLicenseVersion", // Internal property, not serialized + "CurrentLicenseFileVersion", // Constant field, not an instance property + "Hash", // Signature-related, not in claims system + "Signature", // Signature-related, not in claims system + "Token", // The JWT itself, not a claim within the token + "Version" // Not in claims system (only in deprecated property-based licenses) + }; + + // 4. Find license properties without corresponding constants + var propertiesWithoutConstants = licenseProperties + .Except(constants) + .Except(excludedProperties) + .OrderBy(p => p) + .ToList(); + + // 5. Build error message with guidance + var errorMessage = ""; + if (propertiesWithoutConstants.Any()) + { + errorMessage = $"The following OrganizationLicense properties don't have constants in OrganizationLicenseConstants:\n"; + errorMessage += string.Join("\n", propertiesWithoutConstants.Select(p => $" - {p}")); + errorMessage += "\n\nPlease add the following constants to OrganizationLicenseConstants:\n"; + foreach (var prop in propertiesWithoutConstants) + { + errorMessage += $" public const string {prop} = nameof({prop});\n"; + } + } + + // 6. Assert - if this fails, the error message guides the developer to add the constant + Assert.True( + !propertiesWithoutConstants.Any(), + $"\n{errorMessage}"); + } +} diff --git a/test/Core.Test/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactoryTests.cs b/test/Core.Test/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactoryTests.cs new file mode 100644 index 0000000000..43dc416de1 --- /dev/null +++ b/test/Core.Test/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactoryTests.cs @@ -0,0 +1,92 @@ +using System.Reflection; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Billing.Licenses; +using Bit.Core.Billing.Licenses.Models; +using Bit.Core.Billing.Licenses.Services.Implementations; +using Bit.Core.Models.Business; +using Bit.Test.Common.AutoFixture.Attributes; +using Xunit; + +namespace Bit.Core.Test.Billing.Licenses.Services.Implementations; + +public class OrganizationLicenseClaimsFactoryTests +{ + [Theory, BitAutoData] + public async Task GenerateClaims_CreatesClaimsForAllConstants(Organization organization) + { + // This test ensures that when a constant is added to OrganizationLicenseConstants, + // it is also added to the OrganizationLicenseClaimsFactory to generate claims. + // This is the second step in the license synchronization pipeline: + // Property → Constant → Claim → Extraction → Application + + // 1. Populate all nullable properties to ensure claims can be generated + // The factory only adds claims for properties that have values + organization.Name = "Test Organization"; + organization.BillingEmail = "billing@test.com"; + organization.BusinessName = "Test Business"; + organization.Plan = "Enterprise"; + organization.LicenseKey = "test-license-key"; + organization.Seats = 100; + organization.MaxCollections = 50; + organization.MaxStorageGb = 10; + organization.SmSeats = 25; + organization.SmServiceAccounts = 10; + organization.ExpirationDate = DateTime.UtcNow.AddYears(1); // Ensure org is not expired + + // Create a LicenseContext with a minimal SubscriptionInfo to trigger conditional claims + // ExpirationWithoutGracePeriod is only generated for active, non-trial, annual subscriptions + var licenseContext = new LicenseContext + { + InstallationId = Guid.NewGuid(), + SubscriptionInfo = new SubscriptionInfo + { + Subscription = new SubscriptionInfo.BillingSubscription(null!) + { + TrialEndDate = DateTime.UtcNow.AddDays(-30), // Trial ended in the past + PeriodStartDate = DateTime.UtcNow, + PeriodEndDate = DateTime.UtcNow.AddDays(365), // Annual subscription (>180 days) + Status = "active" + } + } + }; + + // 2. Generate claims + var factory = new OrganizationLicenseClaimsFactory(); + var claims = await factory.GenerateClaims(organization, licenseContext); + + // 3. Get all constants from OrganizationLicenseConstants + var allConstants = typeof(OrganizationLicenseConstants) + .GetFields(BindingFlags.Public | BindingFlags.Static) + .Where(f => f.IsLiteral && !f.IsInitOnly) + .Select(f => f.GetValue(null) as string) + .ToHashSet(); + + // 4. Get claim types from generated claims + var generatedClaimTypes = claims.Select(c => c.Type).ToHashSet(); + + // 5. Find constants that don't have corresponding claims + var constantsWithoutClaims = allConstants + .Except(generatedClaimTypes) + .OrderBy(c => c) + .ToList(); + + // 6. Build error message with guidance + var errorMessage = ""; + if (constantsWithoutClaims.Any()) + { + errorMessage = $"The following constants in OrganizationLicenseConstants are NOT generated as claims in OrganizationLicenseClaimsFactory:\n"; + errorMessage += string.Join("\n", constantsWithoutClaims.Select(c => $" - {c}")); + errorMessage += "\n\nPlease add the following claims to OrganizationLicenseClaimsFactory.GenerateClaims():\n"; + foreach (var constant in constantsWithoutClaims) + { + errorMessage += $" new(nameof(OrganizationLicenseConstants.{constant}), entity.{constant}.ToString()),\n"; + } + errorMessage += "\nNote: If the property is nullable, you may need to add it conditionally."; + } + + // 7. Assert - if this fails, the error message guides the developer to add claim generation + Assert.True( + !constantsWithoutClaims.Any(), + $"\n{errorMessage}"); + } +} diff --git a/test/Core.Test/Billing/Organizations/Commands/UpdateOrganizationLicenseCommandTests.cs b/test/Core.Test/Billing/Organizations/Commands/UpdateOrganizationLicenseCommandTests.cs index 8befbb000d..46c418e913 100644 --- a/test/Core.Test/Billing/Organizations/Commands/UpdateOrganizationLicenseCommandTests.cs +++ b/test/Core.Test/Billing/Organizations/Commands/UpdateOrganizationLicenseCommandTests.cs @@ -1,9 +1,14 @@ -using System.Security.Claims; +using System.Reflection; +using System.Security.Claims; +using System.Text.RegularExpressions; using Bit.Core.AdminConsole.Entities; +using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Licenses; using Bit.Core.Billing.Organizations.Commands; using Bit.Core.Billing.Organizations.Models; using Bit.Core.Billing.Services; using Bit.Core.Enums; +using Bit.Core.Exceptions; using Bit.Core.Models.Data.Organizations; using Bit.Core.Services; using Bit.Core.Settings; @@ -99,6 +104,320 @@ public class UpdateOrganizationLicenseCommandTests } } + [Theory, BitAutoData] + public async Task UpdateLicenseAsync_WithClaimsPrincipal_ExtractsAllPropertiesFromClaims( + SelfHostedOrganizationDetails selfHostedOrg, + OrganizationLicense license, + SutProvider sutProvider) + { + var globalSettings = sutProvider.GetDependency(); + globalSettings.LicenseDirectory = LicenseDirectory; + globalSettings.SelfHosted = true; + + // Setup license for CanUse validation + license.Enabled = true; + license.Issued = DateTime.Now.AddDays(-1); + license.Expires = DateTime.Now.AddDays(1); + license.Version = OrganizationLicense.CurrentLicenseFileVersion; + license.InstallationId = globalSettings.Installation.Id; + license.LicenseType = LicenseType.Organization; + license.Token = "test-token"; // Indicates this is a claims-based license + sutProvider.GetDependency().VerifyLicense(license).Returns(true); + + // Create a ClaimsPrincipal with all organization license claims + var claims = new List + { + new(OrganizationLicenseConstants.LicenseType, ((int)LicenseType.Organization).ToString()), + new(OrganizationLicenseConstants.InstallationId, globalSettings.Installation.Id.ToString()), + new(OrganizationLicenseConstants.Name, "Test Organization"), + new(OrganizationLicenseConstants.BillingEmail, "billing@test.com"), + new(OrganizationLicenseConstants.BusinessName, "Test Business"), + new(OrganizationLicenseConstants.PlanType, ((int)PlanType.EnterpriseAnnually).ToString()), + new(OrganizationLicenseConstants.Seats, "100"), + new(OrganizationLicenseConstants.MaxCollections, "50"), + new(OrganizationLicenseConstants.UsePolicies, "true"), + new(OrganizationLicenseConstants.UseSso, "true"), + new(OrganizationLicenseConstants.UseKeyConnector, "true"), + new(OrganizationLicenseConstants.UseScim, "true"), + new(OrganizationLicenseConstants.UseGroups, "true"), + new(OrganizationLicenseConstants.UseDirectory, "true"), + new(OrganizationLicenseConstants.UseEvents, "true"), + new(OrganizationLicenseConstants.UseTotp, "true"), + new(OrganizationLicenseConstants.Use2fa, "true"), + new(OrganizationLicenseConstants.UseApi, "true"), + new(OrganizationLicenseConstants.UseResetPassword, "true"), + new(OrganizationLicenseConstants.Plan, "Enterprise"), + new(OrganizationLicenseConstants.SelfHost, "true"), + new(OrganizationLicenseConstants.UsersGetPremium, "true"), + new(OrganizationLicenseConstants.UseCustomPermissions, "true"), + new(OrganizationLicenseConstants.Enabled, "true"), + new(OrganizationLicenseConstants.Expires, DateTime.Now.AddDays(1).ToString("O")), + new(OrganizationLicenseConstants.LicenseKey, "test-license-key"), + new(OrganizationLicenseConstants.UsePasswordManager, "true"), + new(OrganizationLicenseConstants.UseSecretsManager, "true"), + new(OrganizationLicenseConstants.SmSeats, "25"), + new(OrganizationLicenseConstants.SmServiceAccounts, "10"), + new(OrganizationLicenseConstants.UseRiskInsights, "true"), + new(OrganizationLicenseConstants.UseOrganizationDomains, "true"), + new(OrganizationLicenseConstants.UseAdminSponsoredFamilies, "true"), + new(OrganizationLicenseConstants.UseAutomaticUserConfirmation, "true"), + new(OrganizationLicenseConstants.UseDisableSmAdsForUsers, "true"), + new(OrganizationLicenseConstants.UsePhishingBlocker, "true"), + new(OrganizationLicenseConstants.MaxStorageGb, "5"), + new(OrganizationLicenseConstants.Issued, DateTime.Now.AddDays(-1).ToString("O")), + new(OrganizationLicenseConstants.Refresh, DateTime.Now.AddMonths(1).ToString("O")), + new(OrganizationLicenseConstants.ExpirationWithoutGracePeriod, DateTime.Now.AddMonths(12).ToString("O")), + new(OrganizationLicenseConstants.Trial, "false"), + new(OrganizationLicenseConstants.LimitCollectionCreationDeletion, "true"), + new(OrganizationLicenseConstants.AllowAdminAccessToAllCollectionItems, "true") + }; + var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity(claims)); + + sutProvider.GetDependency() + .GetClaimsPrincipalFromLicense(license) + .Returns(claimsPrincipal); + + // Setup selfHostedOrg for CanUseLicense validation + selfHostedOrg.OccupiedSeatCount = 50; // Less than the 100 seats in the license + selfHostedOrg.CollectionCount = 10; // Less than the 50 max collections in the license + selfHostedOrg.GroupCount = 1; + selfHostedOrg.UseGroups = true; + selfHostedOrg.UsePolicies = true; + selfHostedOrg.UseSso = true; + selfHostedOrg.UseKeyConnector = true; + selfHostedOrg.UseScim = true; + selfHostedOrg.UseCustomPermissions = true; + selfHostedOrg.UseResetPassword = true; + + try + { + await sutProvider.Sut.UpdateLicenseAsync(selfHostedOrg, license, null); + + // Assertion: license file should be written to disk + var filePath = Path.Combine(LicenseDirectory, "organization", $"{selfHostedOrg.Id}.json"); + await using var fs = File.OpenRead(filePath); + var licenseFromFile = await JsonSerializer.DeserializeAsync(fs); + + AssertHelper.AssertPropertyEqual(license, licenseFromFile, "SignatureBytes"); + + // Assertion: organization should be updated with ALL properties extracted from claims + await sutProvider.GetDependency() + .Received(1) + .ReplaceAndUpdateCacheAsync(Arg.Is(org => + org.Name == "Test Organization" && + org.BillingEmail == "billing@test.com" && + org.BusinessName == "Test Business" && + org.PlanType == PlanType.EnterpriseAnnually && + org.Seats == 100 && + org.MaxCollections == 50 && + org.UsePolicies == true && + org.UseSso == true && + org.UseKeyConnector == true && + org.UseScim == true && + org.UseGroups == true && + org.UseDirectory == true && + org.UseEvents == true && + org.UseTotp == true && + org.Use2fa == true && + org.UseApi == true && + org.UseResetPassword == true && + org.Plan == "Enterprise" && + org.SelfHost == true && + org.UsersGetPremium == true && + org.UseCustomPermissions == true && + org.Enabled == true && + org.LicenseKey == "test-license-key" && + org.UsePasswordManager == true && + org.UseSecretsManager == true && + org.SmSeats == 25 && + org.SmServiceAccounts == 10 && + org.UseRiskInsights == true && + org.UseOrganizationDomains == true && + org.UseAdminSponsoredFamilies == true && + org.UseAutomaticUserConfirmation == true && + org.UseDisableSmAdsForUsers == true && + org.UsePhishingBlocker == true)); + } + finally + { + // Clean up temporary directory + if (Directory.Exists(OrganizationLicenseDirectory.Value)) + { + Directory.Delete(OrganizationLicenseDirectory.Value, true); + } + } + } + + [Theory, BitAutoData] + public async Task UpdateLicenseAsync_WrongInstallationIdInClaims_ThrowsBadRequestException( + SelfHostedOrganizationDetails selfHostedOrg, + OrganizationLicense license, + SutProvider sutProvider) + { + var globalSettings = sutProvider.GetDependency(); + globalSettings.LicenseDirectory = LicenseDirectory; + globalSettings.SelfHosted = true; + + // Setup license for CanUse validation + license.Enabled = true; + license.Issued = DateTime.Now.AddDays(-1); + license.Expires = DateTime.Now.AddDays(1); + license.Version = OrganizationLicense.CurrentLicenseFileVersion; + license.LicenseType = LicenseType.Organization; + license.Token = "test-token"; // Indicates this is a claims-based license + sutProvider.GetDependency().VerifyLicense(license).Returns(true); + + // Create a ClaimsPrincipal with WRONG installation ID + var wrongInstallationId = Guid.NewGuid(); // Different from globalSettings.Installation.Id + var claims = new List + { + new(OrganizationLicenseConstants.LicenseType, ((int)LicenseType.Organization).ToString()), + new(OrganizationLicenseConstants.InstallationId, wrongInstallationId.ToString()), + new(OrganizationLicenseConstants.Enabled, "true"), + new(OrganizationLicenseConstants.SelfHost, "true") + }; + var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity(claims)); + + sutProvider.GetDependency() + .GetClaimsPrincipalFromLicense(license) + .Returns(claimsPrincipal); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.UpdateLicenseAsync(selfHostedOrg, license, null)); + + Assert.Contains("The installation ID does not match the current installation.", exception.Message); + + // Verify organization was NOT saved + await sutProvider.GetDependency() + .DidNotReceive() + .ReplaceAndUpdateCacheAsync(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task UpdateLicenseAsync_ExpiredLicenseWithoutClaims_ThrowsBadRequestException( + SelfHostedOrganizationDetails selfHostedOrg, + OrganizationLicense license, + SutProvider sutProvider) + { + var globalSettings = sutProvider.GetDependency(); + globalSettings.LicenseDirectory = LicenseDirectory; + globalSettings.SelfHosted = true; + + // Setup legacy license (no Token, no claims) + license.Token = null; // Legacy license + license.Enabled = true; + license.Issued = DateTime.Now.AddDays(-2); + license.Expires = DateTime.Now.AddDays(-1); // Expired yesterday + license.Version = OrganizationLicense.CurrentLicenseFileVersion; + license.InstallationId = globalSettings.Installation.Id; + license.LicenseType = LicenseType.Organization; + license.SelfHost = true; + + sutProvider.GetDependency().VerifyLicense(license).Returns(true); + sutProvider.GetDependency() + .GetClaimsPrincipalFromLicense(license) + .Returns((ClaimsPrincipal)null); // No claims for legacy license + + // Passing values for SelfHostedOrganizationDetails.CanUseLicense + license.Seats = null; + license.MaxCollections = null; + license.UseGroups = true; + license.UsePolicies = true; + license.UseSso = true; + license.UseKeyConnector = true; + license.UseScim = true; + license.UseCustomPermissions = true; + license.UseResetPassword = true; + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.UpdateLicenseAsync(selfHostedOrg, license, null)); + + Assert.Contains("The license has expired.", exception.Message); + + // Verify organization was NOT saved + await sutProvider.GetDependency() + .DidNotReceive() + .ReplaceAndUpdateCacheAsync(Arg.Any()); + } + + [Fact] + public async Task UpdateLicenseAsync_ExtractsAllClaimsBasedProperties_WhenClaimsPrincipalProvided() + { + // This test ensures that when new properties are added to OrganizationLicense, + // they are automatically extracted from JWT claims in UpdateOrganizationLicenseCommand. + // If a new constant is added to OrganizationLicenseConstants but not extracted, + // this test will fail with a clear message showing which properties are missing. + + // 1. Get all OrganizationLicenseConstants + var constantFields = typeof(OrganizationLicenseConstants) + .GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.GetField) + .Where(f => f.IsLiteral && !f.IsInitOnly) + .Select(f => f.GetValue(null) as string) + .ToList(); + + // 2. Define properties that should be excluded (not claims-based or intentionally not extracted) + var excludedProperties = new HashSet + { + "Version", // Not in claims system (only in deprecated property-based licenses) + "Hash", // Signature-related, not extracted from claims + "Signature", // Signature-related, not extracted from claims + "SignatureBytes", // Computed from Signature, not a claim + "Token", // The JWT itself, not extracted from claims + "Id" // Cloud org ID from license, not used - self-hosted org has its own separate ID + }; + + // 3. Get properties that should be extracted from claims + var propertiesThatShouldBeExtracted = constantFields + .Where(c => !excludedProperties.Contains(c)) + .ToHashSet(); + + // 4. Read UpdateOrganizationLicenseCommand source code + var commandSourcePath = Path.Combine( + Directory.GetCurrentDirectory(), + "..", "..", "..", "..", "..", + "src", "Core", "Billing", "Organizations", "Commands", "UpdateOrganizationLicenseCommand.cs"); + var sourceCode = await File.ReadAllTextAsync(commandSourcePath); + + // 5. Find all GetValue calls that extract properties from claims + // Pattern matches: license.PropertyName = claimsPrincipal.GetValue(OrganizationLicenseConstants.PropertyName) + var extractedProperties = new HashSet(); + var getValuePattern = @"claimsPrincipal\.GetValue<[^>]+>\(OrganizationLicenseConstants\.(\w+)\)"; + var matches = Regex.Matches(sourceCode, getValuePattern); + + foreach (Match match in matches) + { + extractedProperties.Add(match.Groups[1].Value); + } + + // 6. Find missing extractions + var missingExtractions = propertiesThatShouldBeExtracted + .Except(extractedProperties) + .OrderBy(p => p) + .ToList(); + + // 7. Build error message with guidance if there are missing extractions + var errorMessage = ""; + if (missingExtractions.Any()) + { + errorMessage = $"The following constants in OrganizationLicenseConstants are NOT extracted from claims in UpdateOrganizationLicenseCommand:\n"; + errorMessage += string.Join("\n", missingExtractions.Select(p => $" - {p}")); + errorMessage += "\n\nPlease add the following lines to UpdateOrganizationLicenseCommand.cs in the 'if (claimsPrincipal != null)' block:\n"; + foreach (var prop in missingExtractions) + { + errorMessage += $" license.{prop} = claimsPrincipal.GetValue(OrganizationLicenseConstants.{prop});\n"; + } + } + + // 8. Assert - if this fails, the error message guides the developer to add the extraction + // Note: We don't check for "extra extractions" because that would be a compile error + // (can't reference OrganizationLicenseConstants.Foo if Foo doesn't exist) + Assert.True( + !missingExtractions.Any(), + $"\n{errorMessage}"); + } + // Wrapper to compare 2 objects that are different types private bool AssertPropertyEqual(OrganizationLicense expected, Organization actual, params string[] excludedPropertyStrings) { diff --git a/test/Core.Test/Billing/Premium/Commands/CreatePremiumCloudHostedSubscriptionCommandTests.cs b/test/Core.Test/Billing/Premium/Commands/CreatePremiumCloudHostedSubscriptionCommandTests.cs index 55eb69cc64..da287dc02b 100644 --- a/test/Core.Test/Billing/Premium/Commands/CreatePremiumCloudHostedSubscriptionCommandTests.cs +++ b/test/Core.Test/Billing/Premium/Commands/CreatePremiumCloudHostedSubscriptionCommandTests.cs @@ -266,7 +266,10 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Any()); await _subscriberService.Received(1).CreateBraintreeCustomer(user, paymentMethod.Token); await _stripeAdapter.Received(1).UpdateInvoiceAsync(mockSubscription.LatestInvoiceId, - Arg.Is(opts => opts.AutoAdvance == false)); + Arg.Is(opts => + opts.AutoAdvance == false && + opts.Expand != null && + opts.Expand.Contains("customer"))); await _braintreeService.Received(1).PayInvoice(Arg.Any(), mockInvoice); await _userService.Received(1).SaveUserAsync(user); await _pushNotificationService.Received(1).PushSyncVaultAsync(user.Id); @@ -502,7 +505,10 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests Assert.True(user.Premium); Assert.Equal(mockSubscription.GetCurrentPeriodEnd(), user.PremiumExpirationDate); await _stripeAdapter.Received(1).UpdateInvoiceAsync(mockSubscription.LatestInvoiceId, - Arg.Is(opts => opts.AutoAdvance == false)); + Arg.Is(opts => + opts.AutoAdvance == false && + opts.Expand != null && + opts.Expand.Contains("customer"))); await _braintreeService.Received(1).PayInvoice(Arg.Any(), mockInvoice); } @@ -612,7 +618,10 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests Assert.False(user.Premium); Assert.Null(user.PremiumExpirationDate); await _stripeAdapter.Received(1).UpdateInvoiceAsync(mockSubscription.LatestInvoiceId, - Arg.Is(opts => opts.AutoAdvance == false)); + Arg.Is(opts => + opts.AutoAdvance == false && + opts.Expand != null && + opts.Expand.Contains("customer"))); await _braintreeService.Received(1).PayInvoice(Arg.Any(), mockInvoice); } diff --git a/test/Core.Test/Core.Test.csproj b/test/Core.Test/Core.Test.csproj index b9e218205c..243e9af806 100644 --- a/test/Core.Test/Core.Test.csproj +++ b/test/Core.Test/Core.Test.csproj @@ -2,6 +2,8 @@ false Bit.Core.Test + + $(WarningsNotAsErrors);CA1304;CA1305 @@ -30,7 +32,7 @@ - + diff --git a/test/Core.Test/Services/PlayIdServiceTests.cs b/test/Core.Test/Services/PlayIdServiceTests.cs new file mode 100644 index 0000000000..7e4977c7bb --- /dev/null +++ b/test/Core.Test/Services/PlayIdServiceTests.cs @@ -0,0 +1,211 @@ +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.Services; + +[SutProviderCustomize] +public class PlayIdServiceTests +{ + [Theory] + [BitAutoData] + public void InPlay_WhenPlayIdSetAndDevelopment_ReturnsTrue( + string playId, + SutProvider sutProvider) + { + sutProvider.GetDependency().EnvironmentName.Returns(Environments.Development); + sutProvider.Sut.PlayId = playId; + + var result = sutProvider.Sut.InPlay(out var resultPlayId); + + Assert.True(result); + Assert.Equal(playId, resultPlayId); + } + + [Theory] + [BitAutoData] + public void InPlay_WhenPlayIdSetButNotDevelopment_ReturnsFalse( + string playId, + SutProvider sutProvider) + { + sutProvider.GetDependency().EnvironmentName.Returns(Environments.Production); + sutProvider.Sut.PlayId = playId; + + var result = sutProvider.Sut.InPlay(out var resultPlayId); + + Assert.False(result); + Assert.Equal(playId, resultPlayId); + } + + [Theory] + [BitAutoData((string?)null)] + [BitAutoData("")] + public void InPlay_WhenPlayIdNullOrEmptyAndDevelopment_ReturnsFalse( + string? playId, + SutProvider sutProvider) + { + sutProvider.GetDependency().EnvironmentName.Returns(Environments.Development); + sutProvider.Sut.PlayId = playId; + + var result = sutProvider.Sut.InPlay(out var resultPlayId); + + Assert.False(result); + Assert.Empty(resultPlayId); + } + + [Theory] + [BitAutoData] + public void PlayId_CanGetAndSet(string playId) + { + var hostEnvironment = Substitute.For(); + var sut = new PlayIdService(hostEnvironment); + + sut.PlayId = playId; + + Assert.Equal(playId, sut.PlayId); + } +} + +[SutProviderCustomize] +public class NeverPlayIdServicesTests +{ + [Fact] + public void InPlay_ReturnsFalse() + { + var sut = new NeverPlayIdServices(); + + var result = sut.InPlay(out var playId); + + Assert.False(result); + Assert.Empty(playId); + } + + [Theory] + [InlineData("test-play-id")] + [InlineData(null)] + public void PlayId_SetterDoesNothing_GetterReturnsNull(string? value) + { + var sut = new NeverPlayIdServices(); + + sut.PlayId = value; + + Assert.Null(sut.PlayId); + } +} + +[SutProviderCustomize] +public class PlayIdSingletonServiceTests +{ + public static IEnumerable SutProvider() + { + var sutProvider = new SutProvider(); + var httpContext = sutProvider.CreateDependency(); + var serviceProvider = sutProvider.CreateDependency(); + var hostEnvironment = sutProvider.CreateDependency(); + var playIdService = new PlayIdService(hostEnvironment); + sutProvider.SetDependency(playIdService); + httpContext.RequestServices.Returns(serviceProvider); + serviceProvider.GetService().Returns(playIdService); + serviceProvider.GetRequiredService().Returns(playIdService); + sutProvider.CreateDependency().HttpContext.Returns(httpContext); + sutProvider.Create(); + return [[sutProvider]]; + } + + private void PrepHttpContext( + SutProvider sutProvider) + { + var httpContext = sutProvider.CreateDependency(); + var serviceProvider = sutProvider.CreateDependency(); + var PlayIdService = sutProvider.CreateDependency(); + httpContext.RequestServices.Returns(serviceProvider); + serviceProvider.GetRequiredService().Returns(PlayIdService); + sutProvider.GetDependency().HttpContext.Returns(httpContext); + } + + [Theory] + [BitMemberAutoData(nameof(SutProvider))] + public void InPlay_WhenNoHttpContext_ReturnsFalse( + SutProvider sutProvider) + { + sutProvider.GetDependency().HttpContext.Returns((HttpContext?)null); + sutProvider.GetDependency().EnvironmentName.Returns(Environments.Development); + + var result = sutProvider.Sut.InPlay(out var playId); + + Assert.False(result); + Assert.Empty(playId); + } + + [Theory] + [BitMemberAutoData(nameof(SutProvider))] + public void InPlay_WhenNotDevelopment_ReturnsFalse( + SutProvider sutProvider, + string playIdValue) + { + var scopedPlayIdService = sutProvider.GetDependency(); + scopedPlayIdService.PlayId = playIdValue; + sutProvider.GetDependency().EnvironmentName.Returns(Environments.Production); + + var result = sutProvider.Sut.InPlay(out var playId); + + Assert.False(result); + Assert.Empty(playId); + } + + [Theory] + [BitMemberAutoData(nameof(SutProvider))] + public void InPlay_WhenDevelopmentAndHttpContextWithPlayId_ReturnsTrue( + SutProvider sutProvider, + string playIdValue) + { + sutProvider.GetDependency().PlayId = playIdValue; + sutProvider.GetDependency().EnvironmentName.Returns(Environments.Development); + + var result = sutProvider.Sut.InPlay(out var playId); + + Assert.True(result); + Assert.Equal(playIdValue, playId); + } + + [Theory] + [BitMemberAutoData(nameof(SutProvider))] + public void PlayId_SetterSetsOnScopedService( + SutProvider sutProvider, + string playIdValue) + { + var scopedPlayIdService = sutProvider.GetDependency(); + + sutProvider.Sut.PlayId = playIdValue; + + Assert.Equal(playIdValue, scopedPlayIdService.PlayId); + } + + [Theory] + [BitMemberAutoData(nameof(SutProvider))] + public void PlayId_WhenNoHttpContext_GetterReturnsNull( + SutProvider sutProvider) + { + sutProvider.GetDependency().HttpContext.Returns((HttpContext?)null); + + var result = sutProvider.Sut.PlayId; + + Assert.Null(result); + } + + [Theory] + [BitMemberAutoData(nameof(SutProvider))] + public void PlayId_WhenNoHttpContext_SetterDoesNotThrow( + SutProvider sutProvider, + string playIdValue) + { + sutProvider.GetDependency().HttpContext.Returns((HttpContext?)null); + + sutProvider.Sut.PlayId = playIdValue; + } +} diff --git a/test/Core.Test/Services/PlayItemServiceTests.cs b/test/Core.Test/Services/PlayItemServiceTests.cs new file mode 100644 index 0000000000..02e815be59 --- /dev/null +++ b/test/Core.Test/Services/PlayItemServiceTests.cs @@ -0,0 +1,143 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Entities; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.Services; + +[SutProviderCustomize] +public class PlayItemServiceTests +{ + [Theory] + [BitAutoData] + public async Task Record_User_WhenInPlay_RecordsPlayItem( + string playId, + User user, + SutProvider sutProvider) + { + sutProvider.GetDependency() + .InPlay(out Arg.Any()) + .Returns(x => + { + x[0] = playId; + return true; + }); + + await sutProvider.Sut.Record(user); + + await sutProvider.GetDependency() + .Received(1) + .CreateAsync(Arg.Is(pd => + pd.PlayId == playId && + pd.UserId == user.Id && + pd.OrganizationId == null)); + + sutProvider.GetDependency>() + .Received(1) + .Log( + LogLevel.Information, + Arg.Any(), + Arg.Is(o => o.ToString().Contains(user.Id.ToString()) && o.ToString().Contains(playId)), + null, + Arg.Any>()); + } + + [Theory] + [BitAutoData] + public async Task Record_User_WhenNotInPlay_DoesNotRecordPlayItem( + User user, + SutProvider sutProvider) + { + sutProvider.GetDependency() + .InPlay(out Arg.Any()) + .Returns(x => + { + x[0] = null; + return false; + }); + + await sutProvider.Sut.Record(user); + + await sutProvider.GetDependency() + .DidNotReceive() + .CreateAsync(Arg.Any()); + + sutProvider.GetDependency>() + .DidNotReceive() + .Log( + LogLevel.Information, + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any>()); + } + + [Theory] + [BitAutoData] + public async Task Record_Organization_WhenInPlay_RecordsPlayItem( + string playId, + Organization organization, + SutProvider sutProvider) + { + sutProvider.GetDependency() + .InPlay(out Arg.Any()) + .Returns(x => + { + x[0] = playId; + return true; + }); + + await sutProvider.Sut.Record(organization); + + await sutProvider.GetDependency() + .Received(1) + .CreateAsync(Arg.Is(pd => + pd.PlayId == playId && + pd.OrganizationId == organization.Id && + pd.UserId == null)); + + sutProvider.GetDependency>() + .Received(1) + .Log( + LogLevel.Information, + Arg.Any(), + Arg.Is(o => o.ToString().Contains(organization.Id.ToString()) && o.ToString().Contains(playId)), + null, + Arg.Any>()); + } + + [Theory] + [BitAutoData] + public async Task Record_Organization_WhenNotInPlay_DoesNotRecordPlayItem( + Organization organization, + SutProvider sutProvider) + { + sutProvider.GetDependency() + .InPlay(out Arg.Any()) + .Returns(x => + { + x[0] = null; + return false; + }); + + await sutProvider.Sut.Record(organization); + + await sutProvider.GetDependency() + .DidNotReceive() + .CreateAsync(Arg.Any()); + + sutProvider.GetDependency>() + .DidNotReceive() + .Log( + LogLevel.Information, + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any>()); + } +} diff --git a/test/Core.Test/Tools/Services/SendAuthenticationQueryTests.cs b/test/Core.Test/Tools/Services/SendAuthenticationQueryTests.cs index c34afc42bd..7901b3c5c0 100644 --- a/test/Core.Test/Tools/Services/SendAuthenticationQueryTests.cs +++ b/test/Core.Test/Tools/Services/SendAuthenticationQueryTests.cs @@ -1,4 +1,5 @@ using Bit.Core.Tools.Entities; +using Bit.Core.Tools.Enums; using Bit.Core.Tools.Models.Data; using Bit.Core.Tools.Repositories; using Bit.Core.Tools.SendFeatures.Queries; @@ -47,7 +48,7 @@ public class SendAuthenticationQueryTests { // Arrange var sendId = Guid.NewGuid(); - var send = CreateSend(accessCount: 0, maxAccessCount: 10, emails: emailString, password: null); + var send = CreateSend(accessCount: 0, maxAccessCount: 10, emails: emailString, password: null, AuthType.Email); _sendRepository.GetByIdAsync(sendId).Returns(send); // Act @@ -63,7 +64,7 @@ public class SendAuthenticationQueryTests { // Arrange var sendId = Guid.NewGuid(); - var send = CreateSend(accessCount: 0, maxAccessCount: 10, emails: "test@example.com", password: "hashedpassword"); + var send = CreateSend(accessCount: 0, maxAccessCount: 10, emails: "test@example.com", password: "hashedpassword", AuthType.Email); _sendRepository.GetByIdAsync(sendId).Returns(send); // Act @@ -78,7 +79,7 @@ public class SendAuthenticationQueryTests { // Arrange var sendId = Guid.NewGuid(); - var send = CreateSend(accessCount: 0, maxAccessCount: 10, emails: null, password: null); + var send = CreateSend(accessCount: 0, maxAccessCount: 10, emails: null, password: null, AuthType.None); _sendRepository.GetByIdAsync(sendId).Returns(send); // Act @@ -105,11 +106,11 @@ public class SendAuthenticationQueryTests public static IEnumerable AuthenticationMethodTestCases() { yield return new object[] { null, typeof(NeverAuthenticate) }; - yield return new object[] { CreateSend(accessCount: 5, maxAccessCount: 5, emails: null, password: null), typeof(NeverAuthenticate) }; - yield return new object[] { CreateSend(accessCount: 6, maxAccessCount: 5, emails: null, password: null), typeof(NeverAuthenticate) }; - yield return new object[] { CreateSend(accessCount: 0, maxAccessCount: 10, emails: "test@example.com", password: null), typeof(EmailOtp) }; - yield return new object[] { CreateSend(accessCount: 0, maxAccessCount: 10, emails: null, password: "hashedpassword"), typeof(ResourcePassword) }; - yield return new object[] { CreateSend(accessCount: 0, maxAccessCount: 10, emails: null, password: null), typeof(NotAuthenticated) }; + yield return new object[] { CreateSend(accessCount: 5, maxAccessCount: 5, emails: null, password: null, AuthType.None), typeof(NeverAuthenticate) }; + yield return new object[] { CreateSend(accessCount: 6, maxAccessCount: 5, emails: null, password: null, AuthType.None), typeof(NeverAuthenticate) }; + yield return new object[] { CreateSend(accessCount: 0, maxAccessCount: 10, emails: "test@example.com", password: null, AuthType.Email), typeof(EmailOtp) }; + yield return new object[] { CreateSend(accessCount: 0, maxAccessCount: 10, emails: null, password: "hashedpassword", AuthType.Password), typeof(ResourcePassword) }; + yield return new object[] { CreateSend(accessCount: 0, maxAccessCount: 10, emails: null, password: null, AuthType.None), typeof(NotAuthenticated) }; } public static IEnumerable EmailParsingTestCases() @@ -121,7 +122,7 @@ public class SendAuthenticationQueryTests yield return new object[] { " , test@example.com, ,other@example.com, ", new[] { "test@example.com", "other@example.com" } }; } - private static Send CreateSend(int accessCount, int? maxAccessCount, string? emails, string? password) + private static Send CreateSend(int accessCount, int? maxAccessCount, string? emails, string? password, AuthType? authType) { return new Send { @@ -129,7 +130,8 @@ public class SendAuthenticationQueryTests AccessCount = accessCount, MaxAccessCount = maxAccessCount, Emails = emails, - Password = password + Password = password, + AuthType = authType }; } } diff --git a/test/Core.Test/Tools/Services/SendOwnerQueryTests.cs b/test/Core.Test/Tools/Services/SendOwnerQueryTests.cs index 9a2f942eb8..71ed27f2ac 100644 --- a/test/Core.Test/Tools/Services/SendOwnerQueryTests.cs +++ b/test/Core.Test/Tools/Services/SendOwnerQueryTests.cs @@ -12,7 +12,6 @@ namespace Bit.Core.Test.Tools.Services; public class SendOwnerQueryTests { private readonly ISendRepository _sendRepository; - private readonly IFeatureService _featureService; private readonly IUserService _userService; private readonly SendOwnerQuery _sendOwnerQuery; private readonly Guid _currentUserId = Guid.NewGuid(); @@ -21,11 +20,10 @@ public class SendOwnerQueryTests public SendOwnerQueryTests() { _sendRepository = Substitute.For(); - _featureService = Substitute.For(); _userService = Substitute.For(); _user = new ClaimsPrincipal(); _userService.GetProperUserId(_user).Returns(_currentUserId); - _sendOwnerQuery = new SendOwnerQuery(_sendRepository, _featureService, _userService); + _sendOwnerQuery = new SendOwnerQuery(_sendRepository, _userService); } [Fact] @@ -84,7 +82,7 @@ public class SendOwnerQueryTests } [Fact] - public async Task GetOwned_WithFeatureFlagEnabled_ReturnsAllSends() + public async Task GetOwned_ReturnsAllSendsIncludingEmailOTP() { // Arrange var sends = new List @@ -94,7 +92,6 @@ public class SendOwnerQueryTests CreateSend(Guid.NewGuid(), _currentUserId, emails: "other@example.com") }; _sendRepository.GetManyByUserIdAsync(_currentUserId).Returns(sends); - _featureService.IsEnabled(FeatureFlagKeys.PM19051_ListEmailOtpSends).Returns(true); // Act var result = await _sendOwnerQuery.GetOwned(_user); @@ -105,28 +102,6 @@ public class SendOwnerQueryTests Assert.Contains(sends[1], result); Assert.Contains(sends[2], result); await _sendRepository.Received(1).GetManyByUserIdAsync(_currentUserId); - _featureService.Received(1).IsEnabled(FeatureFlagKeys.PM19051_ListEmailOtpSends); - } - - [Fact] - public async Task GetOwned_WithFeatureFlagDisabled_FiltersOutEmailOtpSends() - { - // Arrange - var sendWithoutEmails = CreateSend(Guid.NewGuid(), _currentUserId, emails: null); - var sendWithEmails = CreateSend(Guid.NewGuid(), _currentUserId, emails: "test@example.com"); - var sends = new List { sendWithoutEmails, sendWithEmails }; - _sendRepository.GetManyByUserIdAsync(_currentUserId).Returns(sends); - _featureService.IsEnabled(FeatureFlagKeys.PM19051_ListEmailOtpSends).Returns(false); - - // Act - var result = await _sendOwnerQuery.GetOwned(_user); - - // Assert - Assert.Single(result); - Assert.Contains(sendWithoutEmails, result); - Assert.DoesNotContain(sendWithEmails, result); - await _sendRepository.Received(1).GetManyByUserIdAsync(_currentUserId); - _featureService.Received(1).IsEnabled(FeatureFlagKeys.PM19051_ListEmailOtpSends); } [Fact] @@ -147,7 +122,6 @@ public class SendOwnerQueryTests // Arrange var emptySends = new List(); _sendRepository.GetManyByUserIdAsync(_currentUserId).Returns(emptySends); - _featureService.IsEnabled(FeatureFlagKeys.PM19051_ListEmailOtpSends).Returns(true); // Act var result = await _sendOwnerQuery.GetOwned(_user); diff --git a/test/Identity.IntegrationTest/Identity.IntegrationTest.csproj b/test/Identity.IntegrationTest/Identity.IntegrationTest.csproj index 5c94fad1d1..8a3c0d0fc2 100644 --- a/test/Identity.IntegrationTest/Identity.IntegrationTest.csproj +++ b/test/Identity.IntegrationTest/Identity.IntegrationTest.csproj @@ -2,6 +2,8 @@ false + + $(WarningsNotAsErrors);CA1304;CA1305 diff --git a/test/Identity.Test/Identity.Test.csproj b/test/Identity.Test/Identity.Test.csproj index 496d652b30..8acb0ced92 100644 --- a/test/Identity.Test/Identity.Test.csproj +++ b/test/Identity.Test/Identity.Test.csproj @@ -2,6 +2,8 @@ false + + $(WarningsNotAsErrors);CA1305 diff --git a/test/Infrastructure.EFIntegration.Test/Infrastructure.EFIntegration.Test.csproj b/test/Infrastructure.EFIntegration.Test/Infrastructure.EFIntegration.Test.csproj index e63d3d7419..c2e0412752 100644 --- a/test/Infrastructure.EFIntegration.Test/Infrastructure.EFIntegration.Test.csproj +++ b/test/Infrastructure.EFIntegration.Test/Infrastructure.EFIntegration.Test.csproj @@ -1,6 +1,8 @@ false + + $(WarningsNotAsErrors);CA1305 diff --git a/test/Infrastructure.EFIntegration.Test/Vault/Repositories/CipherRepositoryTests.cs b/test/Infrastructure.EFIntegration.Test/Vault/Repositories/CipherRepositoryTests.cs index 5aceb15124..a314d15dda 100644 --- a/test/Infrastructure.EFIntegration.Test/Vault/Repositories/CipherRepositoryTests.cs +++ b/test/Infrastructure.EFIntegration.Test/Vault/Repositories/CipherRepositoryTests.cs @@ -2,6 +2,7 @@ using Bit.Core.Entities; using Bit.Core.Models.Data; using Bit.Core.Test.AutoFixture.Attributes; +using Bit.Core.Utilities; using Bit.Core.Vault.Entities; using Bit.Infrastructure.EFIntegration.Test.AutoFixture; using Bit.Infrastructure.EFIntegration.Test.Repositories.EqualityComparers; @@ -279,4 +280,92 @@ public class CipherRepositoryTests Assert.Equal(Core.Vault.Enums.CipherRepromptType.Password, savedCipher.Reprompt); } } + + [CiSkippedTheory, EfUserCipherCustomize, BitAutoData] + public async Task ArchiveAsync_SetsArchivesJsonAndBumpsUserAccountRevisionDate( + Cipher cipher, + User user, + List suts, + List efUserRepos) + { + foreach (var sut in suts) + { + var i = suts.IndexOf(sut); + + var efUser = await efUserRepos[i].CreateAsync(user); + efUserRepos[i].ClearChangeTracking(); + + cipher.UserId = efUser.Id; + cipher.OrganizationId = null; + + var createdCipher = await sut.CreateAsync(cipher); + sut.ClearChangeTracking(); + + var archiveUtcNow = await sut.ArchiveAsync(new[] { createdCipher.Id }, efUser.Id); + sut.ClearChangeTracking(); + + var savedCipher = await sut.GetByIdAsync(createdCipher.Id); + Assert.NotNull(savedCipher); + + Assert.Equal(archiveUtcNow, savedCipher.RevisionDate); + + Assert.False(string.IsNullOrWhiteSpace(savedCipher.Archives)); + var archives = CoreHelpers.LoadClassFromJsonData>(savedCipher.Archives); + Assert.NotNull(archives); + Assert.True(archives.ContainsKey(efUser.Id)); + Assert.Equal(archiveUtcNow, archives[efUser.Id]); + + var bumpedUser = await efUserRepos[i].GetByIdAsync(efUser.Id); + Assert.Equal(DateTime.UtcNow.ToShortDateString(), bumpedUser.AccountRevisionDate.ToShortDateString()); + } + } + + [CiSkippedTheory, EfUserCipherCustomize, BitAutoData] + public async Task UnarchiveAsync_RemovesUserFromArchivesJsonAndBumpsUserAccountRevisionDate( + Cipher cipher, + User user, + List suts, + List efUserRepos) + { + foreach (var sut in suts) + { + var i = suts.IndexOf(sut); + + var efUser = await efUserRepos[i].CreateAsync(user); + efUserRepos[i].ClearChangeTracking(); + + cipher.UserId = efUser.Id; + cipher.OrganizationId = null; + + var createdCipher = await sut.CreateAsync(cipher); + sut.ClearChangeTracking(); + + // Precondition: archived + await sut.ArchiveAsync(new[] { createdCipher.Id }, efUser.Id); + sut.ClearChangeTracking(); + + var unarchiveUtcNow = await sut.UnarchiveAsync(new[] { createdCipher.Id }, efUser.Id); + sut.ClearChangeTracking(); + + var savedCipher = await sut.GetByIdAsync(createdCipher.Id); + Assert.NotNull(savedCipher); + + Assert.Equal(unarchiveUtcNow, savedCipher.RevisionDate); + + // Archives should be null or not contain this user (repo clears string when map empty) + if (!string.IsNullOrWhiteSpace(savedCipher.Archives)) + { + var archives = CoreHelpers.LoadClassFromJsonData>(savedCipher.Archives) + ?? new Dictionary(); + Assert.False(archives.ContainsKey(efUser.Id)); + } + else + { + Assert.Null(savedCipher.Archives); + } + + var bumpedUser = await efUserRepos[i].GetByIdAsync(efUser.Id); + Assert.Equal(DateTime.UtcNow.ToShortDateString(), bumpedUser.AccountRevisionDate.ToShortDateString()); + } + } } diff --git a/test/Infrastructure.IntegrationTest/DatabaseDataAttribute.cs b/test/Infrastructure.IntegrationTest/DatabaseDataAttribute.cs index c458969748..00e3149bbf 100644 --- a/test/Infrastructure.IntegrationTest/DatabaseDataAttribute.cs +++ b/test/Infrastructure.IntegrationTest/DatabaseDataAttribute.cs @@ -128,7 +128,6 @@ public class DatabaseDataAttribute : DataAttribute private void AddDapperServices(IServiceCollection services, Database database) { - services.AddDapperRepositories(SelfHosted); var globalSettings = new GlobalSettings { DatabaseProvider = "sqlServer", @@ -141,6 +140,7 @@ public class DatabaseDataAttribute : DataAttribute UserRequestExpiration = TimeSpan.FromMinutes(15), } }; + services.AddDapperRepositories(SelfHosted); services.AddSingleton(globalSettings); services.AddSingleton(globalSettings); services.AddSingleton(database); @@ -160,7 +160,6 @@ public class DatabaseDataAttribute : DataAttribute private void AddEfServices(IServiceCollection services, Database database) { services.SetupEntityFramework(database.ConnectionString, database.Type); - services.AddPasswordManagerEFRepositories(SelfHosted); var globalSettings = new GlobalSettings { @@ -169,6 +168,7 @@ public class DatabaseDataAttribute : DataAttribute UserRequestExpiration = TimeSpan.FromMinutes(15), }, }; + services.AddPasswordManagerEFRepositories(SelfHosted); services.AddSingleton(globalSettings); services.AddSingleton(globalSettings); diff --git a/test/Infrastructure.IntegrationTest/Infrastructure.IntegrationTest.csproj b/test/Infrastructure.IntegrationTest/Infrastructure.IntegrationTest.csproj index a2215e3453..4822df4c77 100644 --- a/test/Infrastructure.IntegrationTest/Infrastructure.IntegrationTest.csproj +++ b/test/Infrastructure.IntegrationTest/Infrastructure.IntegrationTest.csproj @@ -3,6 +3,8 @@ false 6570f288-5c2c-47ad-8978-f3da255079c2 + + $(WarningsNotAsErrors);CA1305 diff --git a/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs b/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs index 4b42f575a1..dbea807259 100644 --- a/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs +++ b/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs @@ -47,7 +47,7 @@ public abstract class WebApplicationFactoryBase : WebApplicationFactory /// public bool ManagesDatabase { get; set; } = true; - private readonly List> _configureTestServices = new(); + protected readonly List> _configureTestServices = new(); private readonly List> _configureAppConfiguration = new(); public void SubstituteService(Action mockService) diff --git a/test/IntegrationTestCommon/IntegrationTestCommon.csproj b/test/IntegrationTestCommon/IntegrationTestCommon.csproj index 7e04d29248..6fd6369f49 100644 --- a/test/IntegrationTestCommon/IntegrationTestCommon.csproj +++ b/test/IntegrationTestCommon/IntegrationTestCommon.csproj @@ -2,6 +2,8 @@ false + + $(WarningsNotAsErrors);CA1305 diff --git a/test/SeederApi.IntegrationTest/HttpClientExtensions.cs b/test/SeederApi.IntegrationTest/HttpClientExtensions.cs new file mode 100644 index 0000000000..0fd7934208 --- /dev/null +++ b/test/SeederApi.IntegrationTest/HttpClientExtensions.cs @@ -0,0 +1,40 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; + +namespace Bit.SeederApi.IntegrationTest; + +public static class HttpClientExtensions +{ + /// + /// Sends a POST request with JSON content and attaches the x-play-id header. + /// + /// The type of the value to serialize. + /// The HTTP client. + /// The URI the request is sent to. + /// The value to serialize. + /// The play ID to attach as x-play-id header. + /// Options to control the behavior during serialization. + /// A cancellation token that can be used to cancel the operation. + /// The task object representing the asynchronous operation. + public static Task PostAsJsonAsync( + this HttpClient client, + [StringSyntax(StringSyntaxAttribute.Uri)] string? requestUri, + TValue value, + string playId, + JsonSerializerOptions? options = null, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(client); + + if (string.IsNullOrWhiteSpace(playId)) + { + throw new ArgumentException("Play ID cannot be null or whitespace.", nameof(playId)); + } + + var content = JsonContent.Create(value, mediaType: null, options); + content.Headers.Remove("x-play-id"); + content.Headers.Add("x-play-id", playId); + + return client.PostAsync(requestUri, content, cancellationToken); + } +} diff --git a/test/SeederApi.IntegrationTest/QueryControllerTest.cs b/test/SeederApi.IntegrationTest/QueryControllerTest.cs new file mode 100644 index 0000000000..571181e49f --- /dev/null +++ b/test/SeederApi.IntegrationTest/QueryControllerTest.cs @@ -0,0 +1,75 @@ +using System.Net; +using Bit.SeederApi.Models.Request; +using Xunit; + +namespace Bit.SeederApi.IntegrationTest; + +public class QueryControllerTests : IClassFixture, IAsyncLifetime +{ + private readonly HttpClient _client; + private readonly SeederApiApplicationFactory _factory; + + public QueryControllerTests(SeederApiApplicationFactory factory) + { + _factory = factory; + _client = _factory.CreateClient(); + } + + public Task InitializeAsync() + { + return Task.CompletedTask; + } + + public Task DisposeAsync() + { + _client.Dispose(); + return Task.CompletedTask; + } + + [Fact] + public async Task QueryEndpoint_WithValidQueryAndArguments_ReturnsOk() + { + var testEmail = $"emergency-test-{Guid.NewGuid()}@bitwarden.com"; + + var response = await _client.PostAsJsonAsync("/query", new QueryRequestModel + { + Template = "EmergencyAccessInviteQuery", + Arguments = System.Text.Json.JsonSerializer.SerializeToElement(new { email = testEmail }) + }); + + response.EnsureSuccessStatusCode(); + var result = await response.Content.ReadAsStringAsync(); + + Assert.NotNull(result); + + var urls = System.Text.Json.JsonSerializer.Deserialize>(result); + Assert.NotNull(urls); + // For a non-existent email, we expect an empty list + Assert.Empty(urls); + } + + [Fact] + public async Task QueryEndpoint_WithInvalidQueryName_ReturnsNotFound() + { + var response = await _client.PostAsJsonAsync("/query", new QueryRequestModel + { + Template = "NonExistentQuery", + Arguments = System.Text.Json.JsonSerializer.SerializeToElement(new { email = "test@example.com" }) + }); + + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task QueryEndpoint_WithMissingRequiredField_ReturnsBadRequest() + { + // EmergencyAccessInviteQuery requires 'email' field + var response = await _client.PostAsJsonAsync("/query", new QueryRequestModel + { + Template = "EmergencyAccessInviteQuery", + Arguments = System.Text.Json.JsonSerializer.SerializeToElement(new { wrongField = "value" }) + }); + + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + } +} diff --git a/test/SeederApi.IntegrationTest/SeedControllerTest.cs b/test/SeederApi.IntegrationTest/SeedControllerTest.cs new file mode 100644 index 0000000000..1d081d019e --- /dev/null +++ b/test/SeederApi.IntegrationTest/SeedControllerTest.cs @@ -0,0 +1,222 @@ +using System.Net; +using Bit.SeederApi.Models.Request; +using Bit.SeederApi.Models.Response; +using Xunit; + +namespace Bit.SeederApi.IntegrationTest; + +public class SeedControllerTests : IClassFixture, IAsyncLifetime +{ + private readonly HttpClient _client; + private readonly SeederApiApplicationFactory _factory; + + public SeedControllerTests(SeederApiApplicationFactory factory) + { + _factory = factory; + _client = _factory.CreateClient(); + } + + public Task InitializeAsync() + { + return Task.CompletedTask; + } + + public async Task DisposeAsync() + { + // Clean up any seeded data after each test + await _client.DeleteAsync("/seed"); + _client.Dispose(); + } + + [Fact] + public async Task SeedEndpoint_WithValidScene_ReturnsOk() + { + var testEmail = $"seed-test-{Guid.NewGuid()}@bitwarden.com"; + var playId = Guid.NewGuid().ToString(); + + var response = await _client.PostAsJsonAsync("/seed", new SeedRequestModel + { + Template = "SingleUserScene", + Arguments = System.Text.Json.JsonSerializer.SerializeToElement(new { email = testEmail }) + }, playId); + + response.EnsureSuccessStatusCode(); + var result = await response.Content.ReadFromJsonAsync(); + + Assert.NotNull(result); + Assert.NotNull(result.MangleMap); + Assert.Null(result.Result); + } + + [Fact] + public async Task SeedEndpoint_WithInvalidSceneName_ReturnsNotFound() + { + var response = await _client.PostAsJsonAsync("/seed", new SeedRequestModel + { + Template = "NonExistentScene", + Arguments = System.Text.Json.JsonSerializer.SerializeToElement(new { email = "test@example.com" }) + }); + + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task SeedEndpoint_WithMissingRequiredField_ReturnsBadRequest() + { + // SingleUserScene requires 'email' field + var response = await _client.PostAsJsonAsync("/seed", new SeedRequestModel + { + Template = "SingleUserScene", + Arguments = System.Text.Json.JsonSerializer.SerializeToElement(new { wrongField = "value" }) + }); + + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + } + + [Fact] + public async Task DeleteEndpoint_WithValidPlayId_ReturnsOk() + { + var testEmail = $"delete-test-{Guid.NewGuid()}@bitwarden.com"; + var playId = Guid.NewGuid().ToString(); + + var seedResponse = await _client.PostAsJsonAsync("/seed", new SeedRequestModel + { + Template = "SingleUserScene", + Arguments = System.Text.Json.JsonSerializer.SerializeToElement(new { email = testEmail }) + }, playId); + + seedResponse.EnsureSuccessStatusCode(); + var seedResult = await seedResponse.Content.ReadFromJsonAsync(); + Assert.NotNull(seedResult); + + var deleteResponse = await _client.DeleteAsync($"/seed/{playId}"); + deleteResponse.EnsureSuccessStatusCode(); + } + + [Fact] + public async Task DeleteEndpoint_WithInvalidPlayId_ReturnsOk() + { + // DestroyRecipe is idempotent - returns null for non-existent play IDs + var nonExistentPlayId = Guid.NewGuid().ToString(); + var response = await _client.DeleteAsync($"/seed/{nonExistentPlayId}"); + + response.EnsureSuccessStatusCode(); + var content = await response.Content.ReadAsStringAsync(); + Assert.Equal($$"""{"playId":"{{nonExistentPlayId}}"}""", content); + } + + [Fact] + public async Task DeleteBatchEndpoint_WithValidPlayIds_ReturnsOk() + { + // Create multiple seeds with different play IDs + var playIds = new List(); + for (var i = 0; i < 3; i++) + { + var playId = Guid.NewGuid().ToString(); + playIds.Add(playId); + + var testEmail = $"batch-test-{Guid.NewGuid()}@bitwarden.com"; + var seedResponse = await _client.PostAsJsonAsync("/seed", new SeedRequestModel + { + Template = "SingleUserScene", + Arguments = System.Text.Json.JsonSerializer.SerializeToElement(new { email = testEmail }) + }, playId); + + seedResponse.EnsureSuccessStatusCode(); + var seedResult = await seedResponse.Content.ReadFromJsonAsync(); + Assert.NotNull(seedResult); + } + + // Delete them in batch + var request = new HttpRequestMessage(HttpMethod.Delete, "/seed/batch") + { + Content = JsonContent.Create(playIds) + }; + var deleteResponse = await _client.SendAsync(request); + deleteResponse.EnsureSuccessStatusCode(); + + var result = await deleteResponse.Content.ReadFromJsonAsync(); + Assert.NotNull(result); + Assert.Equal("Batch delete completed successfully", result.Message); + } + + [Fact] + public async Task DeleteBatchEndpoint_WithSomeInvalidIds_ReturnsOk() + { + // DestroyRecipe is idempotent - batch delete succeeds even with non-existent IDs + // Create one valid seed with a play ID + var validPlayId = Guid.NewGuid().ToString(); + var testEmail = $"batch-partial-test-{Guid.NewGuid()}@bitwarden.com"; + + var seedResponse = await _client.PostAsJsonAsync("/seed", new SeedRequestModel + { + Template = "SingleUserScene", + Arguments = System.Text.Json.JsonSerializer.SerializeToElement(new { email = testEmail }) + }, validPlayId); + + seedResponse.EnsureSuccessStatusCode(); + var seedResult = await seedResponse.Content.ReadFromJsonAsync(); + Assert.NotNull(seedResult); + + // Try to delete with mix of valid and invalid IDs + var playIds = new List { validPlayId, Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }; + var request = new HttpRequestMessage(HttpMethod.Delete, "/seed/batch") + { + Content = JsonContent.Create(playIds) + }; + var deleteResponse = await _client.SendAsync(request); + + deleteResponse.EnsureSuccessStatusCode(); + var result = await deleteResponse.Content.ReadFromJsonAsync(); + Assert.NotNull(result); + Assert.Equal("Batch delete completed successfully", result.Message); + } + + [Fact] + public async Task DeleteAllEndpoint_DeletesAllSeededData() + { + // Create multiple seeds + for (var i = 0; i < 2; i++) + { + var playId = Guid.NewGuid().ToString(); + var testEmail = $"deleteall-test-{Guid.NewGuid()}@bitwarden.com"; + + var seedResponse = await _client.PostAsJsonAsync("/seed", new SeedRequestModel + { + Template = "SingleUserScene", + Arguments = System.Text.Json.JsonSerializer.SerializeToElement(new { email = testEmail }) + }, playId); + + seedResponse.EnsureSuccessStatusCode(); + } + + // Delete all + var deleteResponse = await _client.DeleteAsync("/seed"); + Assert.Equal(HttpStatusCode.NoContent, deleteResponse.StatusCode); + } + + [Fact] + public async Task SeedEndpoint_VerifyResponseContainsMangleMapAndResult() + { + var testEmail = $"verify-response-{Guid.NewGuid()}@bitwarden.com"; + var playId = Guid.NewGuid().ToString(); + + var response = await _client.PostAsJsonAsync("/seed", new SeedRequestModel + { + Template = "SingleUserScene", + Arguments = System.Text.Json.JsonSerializer.SerializeToElement(new { email = testEmail }) + }, playId); + + response.EnsureSuccessStatusCode(); + var jsonString = await response.Content.ReadAsStringAsync(); + + // Verify the response contains MangleMap and Result fields + Assert.Contains("mangleMap", jsonString, StringComparison.OrdinalIgnoreCase); + Assert.Contains("result", jsonString, StringComparison.OrdinalIgnoreCase); + } + + private class BatchDeleteResponse + { + public string? Message { get; set; } + } +} diff --git a/test/SeederApi.IntegrationTest/SeederApi.IntegrationTest.csproj b/test/SeederApi.IntegrationTest/SeederApi.IntegrationTest.csproj new file mode 100644 index 0000000000..a4709ea58a --- /dev/null +++ b/test/SeederApi.IntegrationTest/SeederApi.IntegrationTest.csproj @@ -0,0 +1,29 @@ + + + false + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + %(RecursiveDir)%(Filename)%(Extension) + PreserveNewest + + + diff --git a/test/SeederApi.IntegrationTest/SeederApiApplicationFactory.cs b/test/SeederApi.IntegrationTest/SeederApiApplicationFactory.cs new file mode 100644 index 0000000000..6d815b03ea --- /dev/null +++ b/test/SeederApi.IntegrationTest/SeederApiApplicationFactory.cs @@ -0,0 +1,18 @@ +using Bit.Core.Services; +using Bit.IntegrationTestCommon; +using Bit.IntegrationTestCommon.Factories; + +namespace Bit.SeederApi.IntegrationTest; + +public class SeederApiApplicationFactory : WebApplicationFactoryBase +{ + public SeederApiApplicationFactory() + { + TestDatabase = new SqliteTestDatabase(); + _configureTestServices.Add(serviceCollection => + { + serviceCollection.AddSingleton(); + serviceCollection.AddHttpContextAccessor(); + }); + } +} diff --git a/test/Server.IntegrationTest/Properties/AssemblyInfo.cs b/test/Server.IntegrationTest/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..80afc76e2e --- /dev/null +++ b/test/Server.IntegrationTest/Properties/AssemblyInfo.cs @@ -0,0 +1 @@ +[assembly: CaptureTrace] diff --git a/test/Server.IntegrationTest/Server.IntegrationTest.csproj b/test/Server.IntegrationTest/Server.IntegrationTest.csproj new file mode 100644 index 0000000000..362ada84a0 --- /dev/null +++ b/test/Server.IntegrationTest/Server.IntegrationTest.csproj @@ -0,0 +1,23 @@ + + + + Exe + enable + + + + + + + + + + + + + + + + + + diff --git a/test/Server.IntegrationTest/Server.cs b/test/Server.IntegrationTest/Server.cs new file mode 100644 index 0000000000..073dbffb5a --- /dev/null +++ b/test/Server.IntegrationTest/Server.cs @@ -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 +{ + 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 + { + "/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([.. args]) + ?? throw new InvalidProgramException("Could not create builder from assembly."); + + builder.UseSetting("TEST_CONTENTROOT_SERVER", ContentRoot); + return builder; + } +} diff --git a/test/Server.IntegrationTest/ServerTests.cs b/test/Server.IntegrationTest/ServerTests.cs new file mode 100644 index 0000000000..e432f53775 --- /dev/null +++ b/test/Server.IntegrationTest/ServerTests.cs @@ -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", ""); + 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("", 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); + } + } +} diff --git a/test/SharedWeb.Test/PlayIdMiddlewareTests.cs b/test/SharedWeb.Test/PlayIdMiddlewareTests.cs new file mode 100644 index 0000000000..f9ed23c36f --- /dev/null +++ b/test/SharedWeb.Test/PlayIdMiddlewareTests.cs @@ -0,0 +1,102 @@ +using Bit.Core.Services; +using Bit.SharedWeb.Utilities; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Hosting; +using NSubstitute; + +namespace SharedWeb.Test; + +public class PlayIdMiddlewareTests +{ + private readonly PlayIdService _playIdService; + private readonly RequestDelegate _next; + private readonly PlayIdMiddleware _middleware; + + public PlayIdMiddlewareTests() + { + var hostEnvironment = Substitute.For(); + hostEnvironment.EnvironmentName.Returns(Environments.Development); + + _playIdService = new PlayIdService(hostEnvironment); + _next = Substitute.For(); + _middleware = new PlayIdMiddleware(_next); + } + + [Fact] + public async Task Invoke_WithValidPlayId_SetsPlayIdAndCallsNext() + { + var context = new DefaultHttpContext(); + context.Request.Headers["x-play-id"] = "test-play-id"; + + await _middleware.Invoke(context, _playIdService); + + Assert.Equal("test-play-id", _playIdService.PlayId); + await _next.Received(1).Invoke(context); + } + + [Fact] + public async Task Invoke_WithoutPlayIdHeader_CallsNext() + { + var context = new DefaultHttpContext(); + + await _middleware.Invoke(context, _playIdService); + + Assert.Null(_playIdService.PlayId); + await _next.Received(1).Invoke(context); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData("\t")] + public async Task Invoke_WithEmptyOrWhitespacePlayId_Returns400(string playId) + { + var context = new DefaultHttpContext(); + context.Response.Body = new MemoryStream(); + context.Request.Headers["x-play-id"] = playId; + + await _middleware.Invoke(context, _playIdService); + + Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode); + await _next.DidNotReceive().Invoke(context); + } + + [Fact] + public async Task Invoke_WithPlayIdExceedingMaxLength_Returns400() + { + var context = new DefaultHttpContext(); + context.Response.Body = new MemoryStream(); + var longPlayId = new string('a', 257); // Exceeds 256 character limit + context.Request.Headers["x-play-id"] = longPlayId; + + await _middleware.Invoke(context, _playIdService); + + Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode); + await _next.DidNotReceive().Invoke(context); + } + + [Fact] + public async Task Invoke_WithPlayIdAtMaxLength_SetsPlayIdAndCallsNext() + { + var context = new DefaultHttpContext(); + var maxLengthPlayId = new string('a', 256); // Exactly 256 characters + context.Request.Headers["x-play-id"] = maxLengthPlayId; + + await _middleware.Invoke(context, _playIdService); + + Assert.Equal(maxLengthPlayId, _playIdService.PlayId); + await _next.Received(1).Invoke(context); + } + + [Fact] + public async Task Invoke_WithSpecialCharactersInPlayId_SetsPlayIdAndCallsNext() + { + var context = new DefaultHttpContext(); + context.Request.Headers["x-play-id"] = "test-play_id.123"; + + await _middleware.Invoke(context, _playIdService); + + Assert.Equal("test-play_id.123", _playIdService.PlayId); + await _next.Received(1).Invoke(context); + } +} diff --git a/util/Migrator/DbScripts/2026-01-08_00_CreatePlayItem.sql b/util/Migrator/DbScripts/2026-01-08_00_CreatePlayItem.sql new file mode 100644 index 0000000000..789538cb1c --- /dev/null +++ b/util/Migrator/DbScripts/2026-01-08_00_CreatePlayItem.sql @@ -0,0 +1,90 @@ +-- Create PlayItem table +IF OBJECT_ID('dbo.PlayItem') IS NULL +BEGIN + CREATE TABLE [dbo].[PlayItem] ( + [Id] UNIQUEIDENTIFIER NOT NULL, + [PlayId] NVARCHAR (256) NOT NULL, + [UserId] UNIQUEIDENTIFIER NULL, + [OrganizationId] UNIQUEIDENTIFIER NULL, + [CreationDate] DATETIME2 (7) NOT NULL, + CONSTRAINT [PK_PlayItem] PRIMARY KEY CLUSTERED ([Id] ASC), + CONSTRAINT [FK_PlayItem_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id]) ON DELETE CASCADE, + CONSTRAINT [FK_PlayItem_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]) ON DELETE CASCADE, + CONSTRAINT [CK_PlayItem_UserOrOrganization] CHECK (([UserId] IS NOT NULL AND [OrganizationId] IS NULL) OR ([UserId] IS NULL AND [OrganizationId] IS NOT NULL)) + ); + + CREATE NONCLUSTERED INDEX [IX_PlayItem_PlayId] + ON [dbo].[PlayItem]([PlayId] ASC); + + CREATE NONCLUSTERED INDEX [IX_PlayItem_UserId] + ON [dbo].[PlayItem]([UserId] ASC); + + CREATE NONCLUSTERED INDEX [IX_PlayItem_OrganizationId] + ON [dbo].[PlayItem]([OrganizationId] ASC); +END +GO + +-- Create PlayItem_Create stored procedure +CREATE OR ALTER PROCEDURE [dbo].[PlayItem_Create] + @Id UNIQUEIDENTIFIER OUTPUT, + @PlayId NVARCHAR(256), + @UserId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @CreationDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[PlayItem] + ( + [Id], + [PlayId], + [UserId], + [OrganizationId], + [CreationDate] + ) + VALUES + ( + @Id, + @PlayId, + @UserId, + @OrganizationId, + @CreationDate + ) +END +GO + +-- Create PlayItem_ReadByPlayId stored procedure +CREATE OR ALTER PROCEDURE [dbo].[PlayItem_ReadByPlayId] + @PlayId NVARCHAR(256) +AS +BEGIN + SET NOCOUNT ON + + SELECT + [Id], + [PlayId], + [UserId], + [OrganizationId], + [CreationDate] + FROM + [dbo].[PlayItem] + WHERE + [PlayId] = @PlayId +END +GO + +-- Create PlayItem_DeleteByPlayId stored procedure +CREATE OR ALTER PROCEDURE [dbo].[PlayItem_DeleteByPlayId] + @PlayId NVARCHAR(256) +AS +BEGIN + SET NOCOUNT ON + + DELETE + FROM + [dbo].[PlayItem] + WHERE + [PlayId] = @PlayId +END +GO diff --git a/util/Migrator/DbScripts/2026-01-12_00_UpdateCipherArchive.sql b/util/Migrator/DbScripts/2026-01-12_00_UpdateCipherArchive.sql new file mode 100644 index 0000000000..2751bb1408 --- /dev/null +++ b/util/Migrator/DbScripts/2026-01-12_00_UpdateCipherArchive.sql @@ -0,0 +1,89 @@ +CREATE OR ALTER PROCEDURE [dbo].[Cipher_Unarchive] + @Ids AS [dbo].[GuidIdArray] READONLY, + @UserId AS UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + CREATE TABLE #Temp + ( + [Id] UNIQUEIDENTIFIER NOT NULL, + [UserId] UNIQUEIDENTIFIER NULL + ) + + INSERT INTO #Temp + SELECT + ucd.[Id], + ucd.[UserId] + FROM + [dbo].[UserCipherDetails](@UserId) ucd + INNER JOIN @Ids ids ON ids.Id = ucd.[Id] + WHERE + ucd.[ArchivedDate] IS NOT NULL + + DECLARE @UtcNow DATETIME2(7) = SYSUTCDATETIME(); + UPDATE + [dbo].[Cipher] + SET + [Archives] = JSON_MODIFY( + COALESCE([Archives], N'{}'), + CONCAT('$."', @UserId, '"'), + NULL + ), + [RevisionDate] = @UtcNow + FROM [dbo].[Cipher] AS c + INNER JOIN #Temp AS t + ON t.[Id] = c.[Id]; + + EXEC [dbo].[User_BumpAccountRevisionDate] @UserId + + DROP TABLE #Temp + + SELECT @UtcNow +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[Cipher_Archive] + @Ids AS [dbo].[GuidIdArray] READONLY, + @UserId AS UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + CREATE TABLE #Temp + ( + [Id] UNIQUEIDENTIFIER NOT NULL, + [UserId] UNIQUEIDENTIFIER NULL + ) + + INSERT INTO #Temp + SELECT + ucd.[Id], + ucd.[UserId] + FROM + [dbo].[UserCipherDetails](@UserId) ucd + INNER JOIN @Ids ids ON ids.Id = ucd.[Id] + WHERE + ucd.[ArchivedDate] IS NULL + + DECLARE @UtcNow DATETIME2(7) = SYSUTCDATETIME(); + UPDATE + [dbo].[Cipher] + SET + [Archives] = JSON_MODIFY( + COALESCE([Archives], N'{}'), + CONCAT('$."', @UserId, '"'), + CONVERT(NVARCHAR(30), @UtcNow, 127) + ), + [RevisionDate] = @UtcNow + FROM [dbo].[Cipher] AS c + INNER JOIN #Temp AS t + ON t.[Id] = c.[Id]; + + EXEC [dbo].[User_BumpAccountRevisionDate] @UserId + + DROP TABLE #Temp + + SELECT @UtcNow +END +GO diff --git a/util/Migrator/Migrator.csproj b/util/Migrator/Migrator.csproj index bef6dadfb8..29caf74f39 100644 --- a/util/Migrator/Migrator.csproj +++ b/util/Migrator/Migrator.csproj @@ -1,5 +1,10 @@ + + + $(WarningsNotAsErrors);CA1305 + + diff --git a/util/MySqlMigrations/Migrations/20260108193951_CreatePlayItem.Designer.cs b/util/MySqlMigrations/Migrations/20260108193951_CreatePlayItem.Designer.cs new file mode 100644 index 0000000000..779f8a06f2 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20260108193951_CreatePlayItem.Designer.cs @@ -0,0 +1,3502 @@ +// +using System; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20260108193951_CreatePlayItem")] + partial class CreatePlayItem + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Bit.Core.Dirt.Reports.Models.Data.OrganizationMemberBaseDetail", b => + { + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("CollectionName") + .HasColumnType("longtext"); + + b.Property("Email") + .HasColumnType("longtext"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("GroupName") + .HasColumnType("longtext"); + + b.Property("HidePasswords") + .HasColumnType("tinyint(1)"); + + b.Property("Manage") + .HasColumnType("tinyint(1)"); + + b.Property("ReadOnly") + .HasColumnType("tinyint(1)"); + + b.Property("ResetPasswordKey") + .HasColumnType("longtext"); + + b.Property("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("UserGuid") + .HasColumnType("char(36)"); + + b.Property("UserName") + .HasColumnType("longtext"); + + b.Property("UsesKeyConnector") + .HasColumnType("tinyint(1)"); + + b.ToTable("OrganizationMemberBaseDetails"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("varchar(2)"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("LimitCollectionCreation") + .HasColumnType("tinyint(1)"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("tinyint(1)"); + + b.Property("LimitItemDeletion") + .HasColumnType("tinyint(1)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("int"); + + b.Property("MaxAutoscaleSmSeats") + .HasColumnType("int"); + + b.Property("MaxAutoscaleSmServiceAccounts") + .HasColumnType("int"); + + b.Property("MaxCollections") + .HasColumnType("smallint"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("datetime(6)"); + + b.Property("Plan") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property("PrivateKey") + .HasColumnType("longtext"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("ReferenceData") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Seats") + .HasColumnType("int"); + + b.Property("SelfHost") + .HasColumnType("tinyint(1)"); + + b.Property("SmSeats") + .HasColumnType("int"); + + b.Property("SmServiceAccounts") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("SyncSeats") + .HasColumnType("tinyint(1)"); + + b.Property("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("Use2fa") + .HasColumnType("tinyint(1)"); + + b.Property("UseAdminSponsoredFamilies") + .HasColumnType("tinyint(1)"); + + b.Property("UseApi") + .HasColumnType("tinyint(1)"); + + b.Property("UseAutomaticUserConfirmation") + .HasColumnType("tinyint(1)"); + + b.Property("UseCustomPermissions") + .HasColumnType("tinyint(1)"); + + b.Property("UseDirectory") + .HasColumnType("tinyint(1)"); + + b.Property("UseDisableSmAdsForUsers") + .HasColumnType("tinyint(1)"); + + b.Property("UseEvents") + .HasColumnType("tinyint(1)"); + + b.Property("UseGroups") + .HasColumnType("tinyint(1)"); + + b.Property("UseKeyConnector") + .HasColumnType("tinyint(1)"); + + b.Property("UseOrganizationDomains") + .HasColumnType("tinyint(1)"); + + b.Property("UsePasswordManager") + .HasColumnType("tinyint(1)"); + + b.Property("UsePhishingBlocker") + .HasColumnType("tinyint(1)"); + + b.Property("UsePolicies") + .HasColumnType("tinyint(1)"); + + b.Property("UseResetPassword") + .HasColumnType("tinyint(1)"); + + b.Property("UseRiskInsights") + .HasColumnType("tinyint(1)"); + + b.Property("UseScim") + .HasColumnType("tinyint(1)"); + + b.Property("UseSecretsManager") + .HasColumnType("tinyint(1)"); + + b.Property("UseSso") + .HasColumnType("tinyint(1)"); + + b.Property("UseTotp") + .HasColumnType("tinyint(1)"); + + b.Property("UsersGetPremium") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Enabled") + .HasAnnotation("Npgsql:IndexInclude", new[] { "UseTotp", "UsersGetPremium" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Policy", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("BillingEmail") + .HasColumnType("longtext"); + + b.Property("BillingPhone") + .HasColumnType("longtext"); + + b.Property("BusinessAddress1") + .HasColumnType("longtext"); + + b.Property("BusinessAddress2") + .HasColumnType("longtext"); + + b.Property("BusinessAddress3") + .HasColumnType("longtext"); + + b.Property("BusinessCountry") + .HasColumnType("longtext"); + + b.Property("BusinessName") + .HasColumnType("longtext"); + + b.Property("BusinessTaxNumber") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DiscountId") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasColumnType("longtext"); + + b.Property("GatewaySubscriptionId") + .HasColumnType("longtext"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UseEvents") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Provider", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Settings") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderOrganization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasColumnType("longtext"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId"); + + b.ToTable("ProviderUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessCode") + .HasMaxLength(25) + .HasColumnType("varchar(25)"); + + b.Property("Approved") + .HasColumnType("tinyint(1)"); + + b.Property("AuthenticationDate") + .HasColumnType("datetime(6)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("MasterPasswordHash") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("RequestCountryName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("RequestDeviceIdentifier") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("RequestDeviceType") + .HasColumnType("tinyint unsigned"); + + b.Property("RequestIpAddress") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("ResponseDate") + .HasColumnType("datetime(6)"); + + b.Property("ResponseDeviceId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ResponseDeviceId"); + + b.HasIndex("UserId"); + + b.ToTable("AuthRequest", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("GranteeId") + .HasColumnType("char(36)"); + + b.Property("GrantorId") + .HasColumnType("char(36)"); + + b.Property("KeyEncrypted") + .HasColumnType("longtext"); + + b.Property("LastNotificationDate") + .HasColumnType("datetime(6)"); + + b.Property("RecoveryInitiatedDate") + .HasColumnType("datetime(6)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("WaitTimeDays") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("GranteeId"); + + b.HasIndex("GrantorId"); + + b.ToTable("EmergencyAccess", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ConsumedDate") + .HasColumnType("datetime(6)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.HasKey("Id") + .HasName("PK_Grant") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpirationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Grant", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SsoConfig", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId"); + + b.HasIndex("OrganizationId", "ExternalId") + .IsUnique() + .HasAnnotation("Npgsql:IndexInclude", new[] { "UserId" }) + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SsoUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AaGuid") + .HasColumnType("char(36)"); + + b.Property("Counter") + .HasColumnType("int"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("CredentialId") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EncryptedPrivateKey") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("EncryptedPublicKey") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("EncryptedUserKey") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PublicKey") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SupportsPrf") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("WebAuthnCredential", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ClientOrganizationMigrationRecord", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("GatewayCustomerId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("int"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("Seats") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId", "OrganizationId") + .IsUnique(); + + b.ToTable("ClientOrganizationMigrationRecord", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("InstallationId") + .HasColumnType("char(36)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("InstallationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInstallation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AssignedSeats") + .HasColumnType("int"); + + b.Property("ClientId") + .HasColumnType("char(36)"); + + b.Property("ClientName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Created") + .HasColumnType("datetime(6)"); + + b.Property("InvoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("InvoiceNumber") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PlanName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("Total") + .HasColumnType("decimal(65,30)"); + + b.Property("UsedSeats") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderInvoiceItem", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AllocatedSeats") + .HasColumnType("int"); + + b.Property("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("PurchasedSeats") + .HasColumnType("int"); + + b.Property("SeatMinimum") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("Id", "PlanType") + .IsUnique(); + + b.ToTable("ProviderPlan", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationApplication", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Applications") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegration", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Configuration") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationIntegration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegrationConfiguration", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Configuration") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("EventType") + .HasColumnType("int"); + + b.Property("Filters") + .HasColumnType("longtext"); + + b.Property("OrganizationIntegrationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Template") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationIntegrationId"); + + b.ToTable("OrganizationIntegrationConfiguration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationReport", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ApplicationAtRiskCount") + .HasColumnType("int"); + + b.Property("ApplicationCount") + .HasColumnType("int"); + + b.Property("ApplicationData") + .HasColumnType("longtext"); + + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("CriticalApplicationAtRiskCount") + .HasColumnType("int"); + + b.Property("CriticalApplicationCount") + .HasColumnType("int"); + + b.Property("CriticalMemberAtRiskCount") + .HasColumnType("int"); + + b.Property("CriticalMemberCount") + .HasColumnType("int"); + + b.Property("CriticalPasswordAtRiskCount") + .HasColumnType("int"); + + b.Property("CriticalPasswordCount") + .HasColumnType("int"); + + b.Property("MemberAtRiskCount") + .HasColumnType("int"); + + b.Property("MemberCount") + .HasColumnType("int"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PasswordAtRiskCount") + .HasColumnType("int"); + + b.Property("PasswordCount") + .HasColumnType("int"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SummaryData") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationReport", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Uri") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b => + { + b.Property("Id") + .HasMaxLength(449) + .HasColumnType("varchar(449)"); + + b.Property("AbsoluteExpiration") + .HasColumnType("datetime(6)"); + + b.Property("ExpiresAtTime") + .HasColumnType("datetime(6)"); + + b.Property("SlidingExpirationInSeconds") + .HasColumnType("bigint"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longblob"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpiresAtTime") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DefaultUserCollectionEmail") + .HasColumnType("longtext"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("HidePasswords") + .HasColumnType("tinyint(1)"); + + b.Property("Manage") + .HasColumnType("tinyint(1)"); + + b.Property("ReadOnly") + .HasColumnType("tinyint(1)"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("HidePasswords") + .HasColumnType("tinyint(1)"); + + b.Property("Manage") + .HasColumnType("tinyint(1)"); + + b.Property("ReadOnly") + .HasColumnType("tinyint(1)"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Active") + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("EncryptedPrivateKey") + .HasColumnType("longtext"); + + b.Property("EncryptedPublicKey") + .HasColumnType("longtext"); + + b.Property("EncryptedUserKey") + .HasColumnType("longtext"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "Identifier") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Device", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Event", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ActingUserId") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("Date") + .HasColumnType("datetime(6)"); + + b.Property("DeviceType") + .HasColumnType("tinyint unsigned"); + + b.Property("DomainName") + .HasColumnType("longtext"); + + b.Property("GrantedServiceAccountId") + .HasColumnType("char(36)"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("InstallationId") + .HasColumnType("char(36)"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("PolicyId") + .HasColumnType("char(36)"); + + b.Property("ProjectId") + .HasColumnType("char(36)"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("ProviderOrganizationId") + .HasColumnType("char(36)"); + + b.Property("ProviderUserId") + .HasColumnType("char(36)"); + + b.Property("SecretId") + .HasColumnType("char(36)"); + + b.Property("ServiceAccountId") + .HasColumnType("char(36)"); + + b.Property("SystemUser") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasDatabaseName("IX_Event_DateOrganizationIdUserId") + .HasAnnotation("SqlServer:Clustered", false) + .HasAnnotation("SqlServer:Include", new[] { "ServiceAccountId", "GrantedServiceAccountId" }); + + b.ToTable("Event", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("GroupUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Config") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("JobRunCount") + .HasColumnType("int"); + + b.Property("LastCheckedDate") + .HasColumnType("datetime(6)"); + + b.Property("NextRunDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("VerifiedDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("IsAdminInitiated") + .HasColumnType("tinyint(1)"); + + b.Property("LastSyncDate") + .HasColumnType("datetime(6)"); + + b.Property("Notes") + .HasColumnType("longtext"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("PlanSponsorshipType") + .HasColumnType("tinyint unsigned"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("char(36)"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("char(36)"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("ToDelete") + .HasColumnType("tinyint(1)"); + + b.Property("ValidUntil") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessSecretsManager") + .HasColumnType("tinyint(1)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.Property("ResetPasswordKey") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.PlayItem", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PlayId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("PlayId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PlayItem", null, t => + { + t.HasCheckConstraint("CK_PlayItem_UserOrOrganization", "(\"UserId\" IS NOT NULL AND \"OrganizationId\" IS NULL) OR (\"UserId\" IS NULL AND \"OrganizationId\" IS NOT NULL)"); + }); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessCount") + .HasColumnType("int"); + + b.Property("AuthType") + .HasColumnType("tinyint unsigned"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("DeletionDate") + .HasColumnType("datetime(6)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("Emails") + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("HideEmail") + .HasColumnType("tinyint(1)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("MaxAccessCount") + .HasColumnType("int"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PostalCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Rate") + .HasColumnType("decimal(65,30)"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("varchar(2)"); + + b.HasKey("Id"); + + b.ToTable("TaxRate", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Amount") + .HasColumnType("decimal(65,30)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PaymentMethodType") + .HasColumnType("tinyint unsigned"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("Refunded") + .HasColumnType("tinyint(1)"); + + b.Property("RefundedAmount") + .HasColumnType("decimal(65,30)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccountRevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("AvatarColor") + .HasMaxLength(7) + .HasColumnType("varchar(7)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EmailVerified") + .HasColumnType("tinyint(1)"); + + b.Property("EquivalentDomains") + .HasColumnType("longtext"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("longtext"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("ForcePasswordReset") + .HasColumnType("tinyint(1)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Kdf") + .HasColumnType("tinyint unsigned"); + + b.Property("KdfIterations") + .HasColumnType("int"); + + b.Property("KdfMemory") + .HasColumnType("int"); + + b.Property("KdfParallelism") + .HasColumnType("int"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("LastEmailChangeDate") + .HasColumnType("datetime(6)"); + + b.Property("LastFailedLoginDate") + .HasColumnType("datetime(6)"); + + b.Property("LastKdfChangeDate") + .HasColumnType("datetime(6)"); + + b.Property("LastKeyRotationDate") + .HasColumnType("datetime(6)"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("datetime(6)"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Premium") + .HasColumnType("tinyint(1)"); + + b.Property("PremiumExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("PrivateKey") + .HasColumnType("longtext"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("ReferenceData") + .HasColumnType("longtext"); + + b.Property("RenewalReminderDate") + .HasColumnType("datetime(6)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("SecurityState") + .HasColumnType("longtext"); + + b.Property("SecurityVersion") + .HasColumnType("int"); + + b.Property("SignedPublicKey") + .HasColumnType("longtext"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("varchar(32)"); + + b.Property("UsesKeyConnector") + .HasColumnType("tinyint(1)"); + + b.Property("VerifyDevices") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.UserSignatureKeyPair", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SignatureAlgorithm") + .HasColumnType("tinyint unsigned"); + + b.Property("SigningKey") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("VerifyingKey") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("UserSignatureKeyPair", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Body") + .HasMaxLength(3000) + .HasColumnType("varchar(3000)"); + + b.Property("ClientType") + .HasColumnType("tinyint unsigned"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Global") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Priority") + .HasColumnType("tinyint unsigned"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("TaskId") + .HasColumnType("char(36)"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("TaskId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ClientType", "Global", "UserId", "OrganizationId", "Priority", "CreationDate") + .IsDescending(false, false, false, false, true, true) + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("NotificationId") + .HasColumnType("char(36)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("ReadDate") + .HasColumnType("datetime(6)"); + + b.HasKey("UserId", "NotificationId") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("NotificationId"); + + b.ToTable("NotificationStatus", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Platform.Installation", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("varchar(150)"); + + b.Property("LastActivityDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("varchar(34)"); + + b.Property("Read") + .HasColumnType("tinyint(1)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Write") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ExpireAt") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ServiceAccountId") + .HasColumnType("char(36)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("Note") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.SecretVersion", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("EditorOrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("EditorServiceAccountId") + .HasColumnType("char(36)"); + + b.Property("SecretId") + .HasColumnType("char(36)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("VersionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("EditorOrganizationUserId") + .HasDatabaseName("IX_SecretVersion_EditorOrganizationUserId"); + + b.HasIndex("EditorServiceAccountId") + .HasDatabaseName("IX_SecretVersion_EditorServiceAccountId"); + + b.HasIndex("SecretId") + .HasDatabaseName("IX_SecretVersion_SecretId"); + + b.ToTable("SecretVersion"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Archives") + .HasColumnType("longtext"); + + b.Property("Attachments") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Favorites") + .HasColumnType("longtext"); + + b.Property("Folders") + .HasColumnType("longtext"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Reprompt") + .HasColumnType("tinyint unsigned"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("char(36)"); + + b.Property("SecretsId") + .HasColumnType("char(36)"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Device", "ResponseDevice") + .WithMany() + .HasForeignKey("ResponseDeviceId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("ResponseDevice"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Platform.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Installation"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegrationConfiguration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegration", "OrganizationIntegration") + .WithMany() + .HasForeignKey("OrganizationIntegrationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationIntegration"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationReport", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.PasswordHealthReportApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Collections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("GroupUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Domains") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.PlayItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.UserSignatureKeyPair", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", "Task") + .WithMany() + .HasForeignKey("TaskId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Task"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", "Notification") + .WithMany() + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notification"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ApiKeys") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.SecretVersion", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "EditorOrganizationUser") + .WithMany() + .HasForeignKey("EditorOrganizationUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "EditorServiceAccount") + .WithMany() + .HasForeignKey("EditorServiceAccountId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "Secret") + .WithMany("SecretVersions") + .HasForeignKey("SecretId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EditorOrganizationUser"); + + b.Navigation("EditorServiceAccount"); + + b.Navigation("Secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Ciphers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Ciphers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Folders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany() + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", null) + .WithMany() + .HasForeignKey("ProjectsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", null) + .WithMany() + .HasForeignKey("SecretsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedProject"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedSecret"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ProjectAccessPolicies") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedProject"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany() + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedProject"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("Ciphers"); + + b.Navigation("Collections"); + + b.Navigation("Connections"); + + b.Navigation("Domains"); + + b.Navigation("Groups"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Policies"); + + b.Navigation("SsoConfigs"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Navigation("CollectionUsers"); + + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Navigation("Ciphers"); + + b.Navigation("Folders"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("SecretVersions"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ProjectAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Navigation("CollectionCiphers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/util/MySqlMigrations/Migrations/20260108193951_CreatePlayItem.cs b/util/MySqlMigrations/Migrations/20260108193951_CreatePlayItem.cs new file mode 100644 index 0000000000..d1b69417d4 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20260108193951_CreatePlayItem.cs @@ -0,0 +1,65 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations; + +/// +public partial class CreatePlayItem : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "PlayItem", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + PlayId = table.Column(type: "varchar(256)", maxLength: 256, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + UserId = table.Column(type: "char(36)", nullable: true, collation: "ascii_general_ci"), + OrganizationId = table.Column(type: "char(36)", nullable: true, collation: "ascii_general_ci"), + CreationDate = table.Column(type: "datetime(6)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_PlayItem", x => x.Id); + table.CheckConstraint("CK_PlayItem_UserOrOrganization", "(\"UserId\" IS NOT NULL AND \"OrganizationId\" IS NULL) OR (\"UserId\" IS NULL AND \"OrganizationId\" IS NOT NULL)"); + table.ForeignKey( + name: "FK_PlayItem_Organization_OrganizationId", + column: x => x.OrganizationId, + principalTable: "Organization", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_PlayItem_User_UserId", + column: x => x.UserId, + principalTable: "User", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_PlayItem_OrganizationId", + table: "PlayItem", + column: "OrganizationId"); + + migrationBuilder.CreateIndex( + name: "IX_PlayItem_PlayId", + table: "PlayItem", + column: "PlayId"); + + migrationBuilder.CreateIndex( + name: "IX_PlayItem_UserId", + table: "PlayItem", + column: "UserId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "PlayItem"); + } +} diff --git a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs index b0b88670a1..dda8d5d179 100644 --- a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -282,71 +282,6 @@ namespace Bit.MySqlMigrations.Migrations b.ToTable("Organization", (string)null); }); - modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b => - { - b.Property("Id") - .HasColumnType("char(36)"); - - b.Property("Configuration") - .HasColumnType("longtext"); - - b.Property("CreationDate") - .HasColumnType("datetime(6)"); - - b.Property("OrganizationId") - .HasColumnType("char(36)"); - - b.Property("RevisionDate") - .HasColumnType("datetime(6)"); - - b.Property("Type") - .HasColumnType("int"); - - b.HasKey("Id"); - - b.HasIndex("OrganizationId") - .HasAnnotation("SqlServer:Clustered", false); - - b.HasIndex("OrganizationId", "Type") - .IsUnique() - .HasAnnotation("SqlServer:Clustered", false); - - b.ToTable("OrganizationIntegration", (string)null); - }); - - modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b => - { - b.Property("Id") - .HasColumnType("char(36)"); - - b.Property("Configuration") - .HasColumnType("longtext"); - - b.Property("CreationDate") - .HasColumnType("datetime(6)"); - - b.Property("EventType") - .HasColumnType("int"); - - b.Property("Filters") - .HasColumnType("longtext"); - - b.Property("OrganizationIntegrationId") - .HasColumnType("char(36)"); - - b.Property("RevisionDate") - .HasColumnType("datetime(6)"); - - b.Property("Template") - .HasColumnType("longtext"); - - b.HasKey("Id"); - - b.HasIndex("OrganizationIntegrationId"); - - b.ToTable("OrganizationIntegrationConfiguration", (string)null); - }); - modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => { b.Property("Id") @@ -626,8 +561,8 @@ namespace Bit.MySqlMigrations.Migrations b.Property("Type") .HasColumnType("tinyint unsigned"); - b.Property("WaitTimeDays") - .HasColumnType("int"); + b.Property("WaitTimeDays") + .HasColumnType("smallint"); b.HasKey("Id"); @@ -1015,6 +950,71 @@ namespace Bit.MySqlMigrations.Migrations b.ToTable("OrganizationApplication", (string)null); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegration", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Configuration") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationIntegration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegrationConfiguration", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Configuration") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("EventType") + .HasColumnType("int"); + + b.Property("Filters") + .HasColumnType("longtext"); + + b.Property("OrganizationIntegrationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Template") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationIntegrationId"); + + b.ToTable("OrganizationIntegrationConfiguration", (string)null); + }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationReport", b => { b.Property("Id") @@ -1627,6 +1627,42 @@ namespace Bit.MySqlMigrations.Migrations b.ToTable("OrganizationUser", (string)null); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.PlayItem", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PlayId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("PlayId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PlayItem", null, t => + { + t.HasCheckConstraint("CK_PlayItem_UserOrOrganization", "(\"UserId\" IS NOT NULL AND \"OrganizationId\" IS NULL) OR (\"UserId\" IS NULL AND \"OrganizationId\" IS NOT NULL)"); + }); + }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => { b.Property("Id") @@ -2607,28 +2643,6 @@ namespace Bit.MySqlMigrations.Migrations b.HasDiscriminator().HasValue("user_service_account"); }); - modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b => - { - b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") - .WithMany() - .HasForeignKey("OrganizationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Organization"); - }); - - modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b => - { - b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", "OrganizationIntegration") - .WithMany() - .HasForeignKey("OrganizationIntegrationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("OrganizationIntegration"); - }); - modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => { b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") @@ -2807,6 +2821,28 @@ namespace Bit.MySqlMigrations.Migrations b.Navigation("Organization"); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegrationConfiguration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegration", "OrganizationIntegration") + .WithMany() + .HasForeignKey("OrganizationIntegrationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationIntegration"); + }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationReport", b => { b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") @@ -3003,6 +3039,23 @@ namespace Bit.MySqlMigrations.Migrations b.Navigation("User"); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.PlayItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => { b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") diff --git a/util/MySqlMigrations/MySqlMigrations.csproj b/util/MySqlMigrations/MySqlMigrations.csproj index 641ad90924..b297ea1041 100644 --- a/util/MySqlMigrations/MySqlMigrations.csproj +++ b/util/MySqlMigrations/MySqlMigrations.csproj @@ -2,6 +2,8 @@ 9f1cd3e0-70f2-4921-8068-b2538fd7c3f7 + + $(WarningsNotAsErrors);CA1305 diff --git a/util/PostgresMigrations/Migrations/20260108193909_CreatePlayItem.Designer.cs b/util/PostgresMigrations/Migrations/20260108193909_CreatePlayItem.Designer.cs new file mode 100644 index 0000000000..0586bd8a23 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20260108193909_CreatePlayItem.Designer.cs @@ -0,0 +1,3508 @@ +// +using System; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20260108193909_CreatePlayItem")] + partial class CreatePlayItem + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:CollationDefinition:postgresIndetermanisticCollation", "en-u-ks-primary,en-u-ks-primary,icu,False") + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Bit.Core.Dirt.Reports.Models.Data.OrganizationMemberBaseDetail", b => + { + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("CollectionName") + .HasColumnType("text"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("GroupName") + .HasColumnType("text"); + + b.Property("HidePasswords") + .HasColumnType("boolean"); + + b.Property("Manage") + .HasColumnType("boolean"); + + b.Property("ReadOnly") + .HasColumnType("boolean"); + + b.Property("ResetPasswordKey") + .HasColumnType("text"); + + b.Property("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("UserGuid") + .HasColumnType("uuid"); + + b.Property("UserName") + .HasColumnType("text"); + + b.Property("UsesKeyConnector") + .HasColumnType("boolean"); + + b.ToTable("OrganizationMemberBaseDetails"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("LimitCollectionCreation") + .HasColumnType("boolean"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("boolean"); + + b.Property("LimitItemDeletion") + .HasColumnType("boolean"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("integer"); + + b.Property("MaxAutoscaleSmSeats") + .HasColumnType("integer"); + + b.Property("MaxAutoscaleSmServiceAccounts") + .HasColumnType("integer"); + + b.Property("MaxCollections") + .HasColumnType("smallint"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("timestamp with time zone"); + + b.Property("Plan") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PlanType") + .HasColumnType("smallint"); + + b.Property("PrivateKey") + .HasColumnType("text"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("ReferenceData") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Seats") + .HasColumnType("integer"); + + b.Property("SelfHost") + .HasColumnType("boolean"); + + b.Property("SmSeats") + .HasColumnType("integer"); + + b.Property("SmServiceAccounts") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("SyncSeats") + .HasColumnType("boolean"); + + b.Property("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("Use2fa") + .HasColumnType("boolean"); + + b.Property("UseAdminSponsoredFamilies") + .HasColumnType("boolean"); + + b.Property("UseApi") + .HasColumnType("boolean"); + + b.Property("UseAutomaticUserConfirmation") + .HasColumnType("boolean"); + + b.Property("UseCustomPermissions") + .HasColumnType("boolean"); + + b.Property("UseDirectory") + .HasColumnType("boolean"); + + b.Property("UseDisableSmAdsForUsers") + .HasColumnType("boolean"); + + b.Property("UseEvents") + .HasColumnType("boolean"); + + b.Property("UseGroups") + .HasColumnType("boolean"); + + b.Property("UseKeyConnector") + .HasColumnType("boolean"); + + b.Property("UseOrganizationDomains") + .HasColumnType("boolean"); + + b.Property("UsePasswordManager") + .HasColumnType("boolean"); + + b.Property("UsePhishingBlocker") + .HasColumnType("boolean"); + + b.Property("UsePolicies") + .HasColumnType("boolean"); + + b.Property("UseResetPassword") + .HasColumnType("boolean"); + + b.Property("UseRiskInsights") + .HasColumnType("boolean"); + + b.Property("UseScim") + .HasColumnType("boolean"); + + b.Property("UseSecretsManager") + .HasColumnType("boolean"); + + b.Property("UseSso") + .HasColumnType("boolean"); + + b.Property("UseTotp") + .HasColumnType("boolean"); + + b.Property("UsersGetPremium") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Enabled"); + + NpgsqlIndexBuilderExtensions.IncludeProperties(b.HasIndex("Id", "Enabled"), new[] { "UseTotp", "UsersGetPremium" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Policy", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("BillingEmail") + .HasColumnType("text"); + + b.Property("BillingPhone") + .HasColumnType("text"); + + b.Property("BusinessAddress1") + .HasColumnType("text"); + + b.Property("BusinessAddress2") + .HasColumnType("text"); + + b.Property("BusinessAddress3") + .HasColumnType("text"); + + b.Property("BusinessCountry") + .HasColumnType("text"); + + b.Property("BusinessName") + .HasColumnType("text"); + + b.Property("BusinessTaxNumber") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountId") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasColumnType("text"); + + b.Property("GatewaySubscriptionId") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UseEvents") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("Provider", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Settings") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderOrganization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId"); + + b.ToTable("ProviderUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessCode") + .HasMaxLength(25) + .HasColumnType("character varying(25)"); + + b.Property("Approved") + .HasColumnType("boolean"); + + b.Property("AuthenticationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("MasterPasswordHash") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("RequestCountryName") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RequestDeviceIdentifier") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("RequestDeviceType") + .HasColumnType("smallint"); + + b.Property("RequestIpAddress") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ResponseDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ResponseDeviceId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ResponseDeviceId"); + + b.HasIndex("UserId"); + + b.ToTable("AuthRequest", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("GranteeId") + .HasColumnType("uuid"); + + b.Property("GrantorId") + .HasColumnType("uuid"); + + b.Property("KeyEncrypted") + .HasColumnType("text"); + + b.Property("LastNotificationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RecoveryInitiatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("WaitTimeDays") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("GranteeId"); + + b.HasIndex("GrantorId"); + + b.ToTable("EmergencyAccess", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ConsumedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .IsRequired() + .HasColumnType("text"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id") + .HasName("PK_Grant") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpirationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Grant", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SsoConfig", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId"); + + b.HasIndex("OrganizationId", "ExternalId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + NpgsqlIndexBuilderExtensions.IncludeProperties(b.HasIndex("OrganizationId", "ExternalId"), new[] { "UserId" }); + + b.HasIndex("OrganizationId", "UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SsoUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AaGuid") + .HasColumnType("uuid"); + + b.Property("Counter") + .HasColumnType("integer"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CredentialId") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EncryptedPrivateKey") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("EncryptedPublicKey") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("EncryptedUserKey") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PublicKey") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SupportsPrf") + .HasColumnType("boolean"); + + b.Property("Type") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("WebAuthnCredential", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ClientOrganizationMigrationRecord", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("GatewayCustomerId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("integer"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PlanType") + .HasColumnType("smallint"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("Seats") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId", "OrganizationId") + .IsUnique(); + + b.ToTable("ClientOrganizationMigrationRecord", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("InstallationId") + .HasColumnType("uuid"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("InstallationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInstallation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AssignedSeats") + .HasColumnType("integer"); + + b.Property("ClientId") + .HasColumnType("uuid"); + + b.Property("ClientName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("InvoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("InvoiceNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PlanName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("Total") + .HasColumnType("numeric"); + + b.Property("UsedSeats") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderInvoiceItem", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AllocatedSeats") + .HasColumnType("integer"); + + b.Property("PlanType") + .HasColumnType("smallint"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("PurchasedSeats") + .HasColumnType("integer"); + + b.Property("SeatMinimum") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("Id", "PlanType") + .IsUnique(); + + b.ToTable("ProviderPlan", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationApplication", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Applications") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegration", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Configuration") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationIntegration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegrationConfiguration", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Configuration") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EventType") + .HasColumnType("integer"); + + b.Property("Filters") + .HasColumnType("text"); + + b.Property("OrganizationIntegrationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Template") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationIntegrationId"); + + b.ToTable("OrganizationIntegrationConfiguration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationReport", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationAtRiskCount") + .HasColumnType("integer"); + + b.Property("ApplicationCount") + .HasColumnType("integer"); + + b.Property("ApplicationData") + .HasColumnType("text"); + + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CriticalApplicationAtRiskCount") + .HasColumnType("integer"); + + b.Property("CriticalApplicationCount") + .HasColumnType("integer"); + + b.Property("CriticalMemberAtRiskCount") + .HasColumnType("integer"); + + b.Property("CriticalMemberCount") + .HasColumnType("integer"); + + b.Property("CriticalPasswordAtRiskCount") + .HasColumnType("integer"); + + b.Property("CriticalPasswordCount") + .HasColumnType("integer"); + + b.Property("MemberAtRiskCount") + .HasColumnType("integer"); + + b.Property("MemberCount") + .HasColumnType("integer"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PasswordAtRiskCount") + .HasColumnType("integer"); + + b.Property("PasswordCount") + .HasColumnType("integer"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SummaryData") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationReport", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Uri") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b => + { + b.Property("Id") + .HasMaxLength(449) + .HasColumnType("character varying(449)"); + + b.Property("AbsoluteExpiration") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAtTime") + .HasColumnType("timestamp with time zone"); + + b.Property("SlidingExpirationInSeconds") + .HasColumnType("bigint"); + + b.Property("Value") + .IsRequired() + .HasColumnType("bytea"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpiresAtTime") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DefaultUserCollectionEmail") + .HasColumnType("text"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("HidePasswords") + .HasColumnType("boolean"); + + b.Property("Manage") + .HasColumnType("boolean"); + + b.Property("ReadOnly") + .HasColumnType("boolean"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.Property("HidePasswords") + .HasColumnType("boolean"); + + b.Property("Manage") + .HasColumnType("boolean"); + + b.Property("ReadOnly") + .HasColumnType("boolean"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EncryptedPrivateKey") + .HasColumnType("text"); + + b.Property("EncryptedPublicKey") + .HasColumnType("text"); + + b.Property("EncryptedUserKey") + .HasColumnType("text"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "Identifier") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Device", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Event", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ActingUserId") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("DeviceType") + .HasColumnType("smallint"); + + b.Property("DomainName") + .HasColumnType("text"); + + b.Property("GrantedServiceAccountId") + .HasColumnType("uuid"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("InstallationId") + .HasColumnType("uuid"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.Property("PolicyId") + .HasColumnType("uuid"); + + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("ProviderOrganizationId") + .HasColumnType("uuid"); + + b.Property("ProviderUserId") + .HasColumnType("uuid"); + + b.Property("SecretId") + .HasColumnType("uuid"); + + b.Property("ServiceAccountId") + .HasColumnType("uuid"); + + b.Property("SystemUser") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasDatabaseName("IX_Event_DateOrganizationIdUserId") + .HasAnnotation("SqlServer:Clustered", false) + .HasAnnotation("SqlServer:Include", new[] { "ServiceAccountId", "GrantedServiceAccountId" }); + + b.ToTable("Event", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("GroupUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Config") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("JobRunCount") + .HasColumnType("integer"); + + b.Property("LastCheckedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("NextRunDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("text"); + + b.Property("VerifiedDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAdminInitiated") + .HasColumnType("boolean"); + + b.Property("LastSyncDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PlanSponsorshipType") + .HasColumnType("smallint"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("uuid"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("uuid"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("uuid"); + + b.Property("ToDelete") + .HasColumnType("boolean"); + + b.Property("ValidUntil") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessSecretsManager") + .HasColumnType("boolean"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("ResetPasswordKey") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.PlayItem", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PlayId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("PlayId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PlayItem", null, t => + { + t.HasCheckConstraint("CK_PlayItem_UserOrOrganization", "(\"UserId\" IS NOT NULL AND \"OrganizationId\" IS NULL) OR (\"UserId\" IS NULL AND \"OrganizationId\" IS NOT NULL)"); + }); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessCount") + .HasColumnType("integer"); + + b.Property("AuthType") + .HasColumnType("smallint"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("DeletionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Disabled") + .HasColumnType("boolean"); + + b.Property("Emails") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("HideEmail") + .HasColumnType("boolean"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("MaxAccessCount") + .HasColumnType("integer"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PostalCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Rate") + .HasColumnType("numeric"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.HasKey("Id"); + + b.ToTable("TaxRate", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PaymentMethodType") + .HasColumnType("smallint"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("Refunded") + .HasColumnType("boolean"); + + b.Property("RefundedAmount") + .HasColumnType("numeric"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccountRevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("AvatarColor") + .HasMaxLength(7) + .HasColumnType("character varying(7)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("EmailVerified") + .HasColumnType("boolean"); + + b.Property("EquivalentDomains") + .HasColumnType("text"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("text"); + + b.Property("FailedLoginCount") + .HasColumnType("integer"); + + b.Property("ForcePasswordReset") + .HasColumnType("boolean"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Kdf") + .HasColumnType("smallint"); + + b.Property("KdfIterations") + .HasColumnType("integer"); + + b.Property("KdfMemory") + .HasColumnType("integer"); + + b.Property("KdfParallelism") + .HasColumnType("integer"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("LastEmailChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastFailedLoginDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastKdfChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastKeyRotationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Premium") + .HasColumnType("boolean"); + + b.Property("PremiumExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("PrivateKey") + .HasColumnType("text"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("ReferenceData") + .HasColumnType("text"); + + b.Property("RenewalReminderDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("SecurityState") + .HasColumnType("text"); + + b.Property("SecurityVersion") + .HasColumnType("integer"); + + b.Property("SignedPublicKey") + .HasColumnType("text"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("UsesKeyConnector") + .HasColumnType("boolean"); + + b.Property("VerifyDevices") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.UserSignatureKeyPair", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SignatureAlgorithm") + .HasColumnType("smallint"); + + b.Property("SigningKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("VerifyingKey") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("UserSignatureKeyPair", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Body") + .HasMaxLength(3000) + .HasColumnType("character varying(3000)"); + + b.Property("ClientType") + .HasColumnType("smallint"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Global") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Priority") + .HasColumnType("smallint"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("TaskId") + .HasColumnType("uuid"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("TaskId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ClientType", "Global", "UserId", "OrganizationId", "Priority", "CreationDate") + .IsDescending(false, false, false, false, true, true) + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("NotificationId") + .HasColumnType("uuid"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ReadDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("UserId", "NotificationId") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("NotificationId"); + + b.ToTable("NotificationStatus", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Platform.Installation", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("LastActivityDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("character varying(34)"); + + b.Property("Read") + .HasColumnType("boolean"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Write") + .HasColumnType("boolean"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("ExpireAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("ServiceAccountId") + .HasColumnType("uuid"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.SecretVersion", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("EditorOrganizationUserId") + .HasColumnType("uuid"); + + b.Property("EditorServiceAccountId") + .HasColumnType("uuid"); + + b.Property("SecretId") + .HasColumnType("uuid"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.Property("VersionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("EditorOrganizationUserId") + .HasDatabaseName("IX_SecretVersion_EditorOrganizationUserId"); + + b.HasIndex("EditorServiceAccountId") + .HasDatabaseName("IX_SecretVersion_EditorServiceAccountId"); + + b.HasIndex("SecretId") + .HasDatabaseName("IX_SecretVersion_SecretId"); + + b.ToTable("SecretVersion"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Archives") + .HasColumnType("text"); + + b.Property("Attachments") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Favorites") + .HasColumnType("text"); + + b.Property("Folders") + .HasColumnType("text"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Reprompt") + .HasColumnType("smallint"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("uuid"); + + b.Property("SecretsId") + .HasColumnType("uuid"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Device", "ResponseDevice") + .WithMany() + .HasForeignKey("ResponseDeviceId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("ResponseDevice"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Platform.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Installation"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegrationConfiguration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegration", "OrganizationIntegration") + .WithMany() + .HasForeignKey("OrganizationIntegrationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationIntegration"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationReport", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.PasswordHealthReportApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Collections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("GroupUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Domains") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.PlayItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.UserSignatureKeyPair", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", "Task") + .WithMany() + .HasForeignKey("TaskId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Task"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", "Notification") + .WithMany() + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notification"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ApiKeys") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.SecretVersion", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "EditorOrganizationUser") + .WithMany() + .HasForeignKey("EditorOrganizationUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "EditorServiceAccount") + .WithMany() + .HasForeignKey("EditorServiceAccountId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "Secret") + .WithMany("SecretVersions") + .HasForeignKey("SecretId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EditorOrganizationUser"); + + b.Navigation("EditorServiceAccount"); + + b.Navigation("Secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Ciphers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Ciphers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Folders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany() + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", null) + .WithMany() + .HasForeignKey("ProjectsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", null) + .WithMany() + .HasForeignKey("SecretsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedProject"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedSecret"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ProjectAccessPolicies") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedProject"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany() + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedProject"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("Ciphers"); + + b.Navigation("Collections"); + + b.Navigation("Connections"); + + b.Navigation("Domains"); + + b.Navigation("Groups"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Policies"); + + b.Navigation("SsoConfigs"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Navigation("CollectionUsers"); + + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Navigation("Ciphers"); + + b.Navigation("Folders"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("SecretVersions"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ProjectAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Navigation("CollectionCiphers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/util/PostgresMigrations/Migrations/20260108193909_CreatePlayItem.cs b/util/PostgresMigrations/Migrations/20260108193909_CreatePlayItem.cs new file mode 100644 index 0000000000..1708ab301e --- /dev/null +++ b/util/PostgresMigrations/Migrations/20260108193909_CreatePlayItem.cs @@ -0,0 +1,63 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations; + +/// +public partial class CreatePlayItem : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "PlayItem", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + PlayId = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + UserId = table.Column(type: "uuid", nullable: true), + OrganizationId = table.Column(type: "uuid", nullable: true), + CreationDate = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_PlayItem", x => x.Id); + table.CheckConstraint("CK_PlayItem_UserOrOrganization", "(\"UserId\" IS NOT NULL AND \"OrganizationId\" IS NULL) OR (\"UserId\" IS NULL AND \"OrganizationId\" IS NOT NULL)"); + table.ForeignKey( + name: "FK_PlayItem_Organization_OrganizationId", + column: x => x.OrganizationId, + principalTable: "Organization", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_PlayItem_User_UserId", + column: x => x.UserId, + principalTable: "User", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_PlayItem_OrganizationId", + table: "PlayItem", + column: "OrganizationId"); + + migrationBuilder.CreateIndex( + name: "IX_PlayItem_PlayId", + table: "PlayItem", + column: "PlayId"); + + migrationBuilder.CreateIndex( + name: "IX_PlayItem_UserId", + table: "PlayItem", + column: "UserId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "PlayItem"); + } +} diff --git a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs index 2a0b91e25d..f5419fa319 100644 --- a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -285,71 +285,6 @@ namespace Bit.PostgresMigrations.Migrations b.ToTable("Organization", (string)null); }); - modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b => - { - b.Property("Id") - .HasColumnType("uuid"); - - b.Property("Configuration") - .HasColumnType("text"); - - b.Property("CreationDate") - .HasColumnType("timestamp with time zone"); - - b.Property("OrganizationId") - .HasColumnType("uuid"); - - b.Property("RevisionDate") - .HasColumnType("timestamp with time zone"); - - b.Property("Type") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("OrganizationId") - .HasAnnotation("SqlServer:Clustered", false); - - b.HasIndex("OrganizationId", "Type") - .IsUnique() - .HasAnnotation("SqlServer:Clustered", false); - - b.ToTable("OrganizationIntegration", (string)null); - }); - - modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b => - { - b.Property("Id") - .HasColumnType("uuid"); - - b.Property("Configuration") - .HasColumnType("text"); - - b.Property("CreationDate") - .HasColumnType("timestamp with time zone"); - - b.Property("EventType") - .HasColumnType("integer"); - - b.Property("Filters") - .HasColumnType("text"); - - b.Property("OrganizationIntegrationId") - .HasColumnType("uuid"); - - b.Property("RevisionDate") - .HasColumnType("timestamp with time zone"); - - b.Property("Template") - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("OrganizationIntegrationId"); - - b.ToTable("OrganizationIntegrationConfiguration", (string)null); - }); - modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => { b.Property("Id") @@ -629,8 +564,8 @@ namespace Bit.PostgresMigrations.Migrations b.Property("Type") .HasColumnType("smallint"); - b.Property("WaitTimeDays") - .HasColumnType("integer"); + b.Property("WaitTimeDays") + .HasColumnType("smallint"); b.HasKey("Id"); @@ -1020,6 +955,71 @@ namespace Bit.PostgresMigrations.Migrations b.ToTable("OrganizationApplication", (string)null); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegration", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Configuration") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationIntegration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegrationConfiguration", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Configuration") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EventType") + .HasColumnType("integer"); + + b.Property("Filters") + .HasColumnType("text"); + + b.Property("OrganizationIntegrationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Template") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationIntegrationId"); + + b.ToTable("OrganizationIntegrationConfiguration", (string)null); + }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationReport", b => { b.Property("Id") @@ -1632,6 +1632,42 @@ namespace Bit.PostgresMigrations.Migrations b.ToTable("OrganizationUser", (string)null); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.PlayItem", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PlayId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("PlayId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PlayItem", null, t => + { + t.HasCheckConstraint("CK_PlayItem_UserOrOrganization", "(\"UserId\" IS NOT NULL AND \"OrganizationId\" IS NULL) OR (\"UserId\" IS NULL AND \"OrganizationId\" IS NOT NULL)"); + }); + }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => { b.Property("Id") @@ -2613,28 +2649,6 @@ namespace Bit.PostgresMigrations.Migrations b.HasDiscriminator().HasValue("user_service_account"); }); - modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b => - { - b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") - .WithMany() - .HasForeignKey("OrganizationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Organization"); - }); - - modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b => - { - b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", "OrganizationIntegration") - .WithMany() - .HasForeignKey("OrganizationIntegrationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("OrganizationIntegration"); - }); - modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => { b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") @@ -2813,6 +2827,28 @@ namespace Bit.PostgresMigrations.Migrations b.Navigation("Organization"); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegrationConfiguration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegration", "OrganizationIntegration") + .WithMany() + .HasForeignKey("OrganizationIntegrationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationIntegration"); + }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationReport", b => { b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") @@ -3009,6 +3045,23 @@ namespace Bit.PostgresMigrations.Migrations b.Navigation("User"); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.PlayItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => { b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") diff --git a/util/PostgresMigrations/PostgresMigrations.csproj b/util/PostgresMigrations/PostgresMigrations.csproj index 3496ff67c1..66f3abe769 100644 --- a/util/PostgresMigrations/PostgresMigrations.csproj +++ b/util/PostgresMigrations/PostgresMigrations.csproj @@ -1,5 +1,10 @@ + + + $(WarningsNotAsErrors);CA1305 + + diff --git a/util/RustSdk/RustSdk.csproj b/util/RustSdk/RustSdk.csproj index ba16a55661..14cc017365 100644 --- a/util/RustSdk/RustSdk.csproj +++ b/util/RustSdk/RustSdk.csproj @@ -10,6 +10,14 @@ + + + + + + + Always true @@ -18,23 +26,36 @@ Always true - runtimes/linux-x64/native/libsdk.dylib + runtimes/linux-x64/native/libsdk.so Always true - runtimes/windows-x64/native/libsdk.dylib + runtimes/windows-x64/native/libsdk.dll - - - + + + + Always + true + runtimes/osx-arm64/native/libsdk.dylib + + + Always + true + runtimes/linux-x64/native/libsdk.so + + + Always + true + runtimes/windows-x64/native/libsdk.dll + diff --git a/util/RustSdk/rust-toolchain.toml b/util/RustSdk/rust-toolchain.toml new file mode 100644 index 0000000000..b8889a3bb3 --- /dev/null +++ b/util/RustSdk/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "1.87.0" diff --git a/util/Seeder/Factories/OrganizationSeeder.cs b/util/Seeder/Factories/OrganizationSeeder.cs index 012661501f..3aac87d400 100644 --- a/util/Seeder/Factories/OrganizationSeeder.cs +++ b/util/Seeder/Factories/OrganizationSeeder.cs @@ -1,7 +1,7 @@ using Bit.Core.Billing.Enums; +using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Infrastructure.EntityFramework.AdminConsole.Models; -using Bit.Infrastructure.EntityFramework.Models; namespace Bit.Seeder.Factories; diff --git a/util/Seeder/Factories/UserSeeder.cs b/util/Seeder/Factories/UserSeeder.cs index b24f8273b9..4fc456981c 100644 --- a/util/Seeder/Factories/UserSeeder.cs +++ b/util/Seeder/Factories/UserSeeder.cs @@ -1,13 +1,59 @@ -using Bit.Core.Enums; -using Bit.Infrastructure.EntityFramework.Models; +using System.Globalization; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Utilities; using Bit.RustSDK; using Microsoft.AspNetCore.Identity; namespace Bit.Seeder.Factories; -public class UserSeeder +public struct UserData { - public static User CreateUser(string email) + public string Email; + public Guid Id; + public string? Key; + public string? PublicKey; + public string? PrivateKey; + public string? ApiKey; + public KdfType Kdf; + public int KdfIterations; +} + +public class UserSeeder(RustSdkService sdkService, IPasswordHasher passwordHasher, MangleId mangleId) +{ + private string MangleEmail(string email) + { + return $"{mangleId}+{email}"; + } + + public User CreateUser(string email, bool emailVerified = false, bool premium = false) + { + email = MangleEmail(email); + var keys = sdkService.GenerateUserKeys(email, "asdfasdfasdf"); + + var user = new User + { + Id = CoreHelpers.GenerateComb(), + Email = email, + EmailVerified = emailVerified, + MasterPassword = null, + SecurityStamp = "4830e359-e150-4eae-be2a-996c81c5e609", + Key = keys.EncryptedUserKey, + PublicKey = keys.PublicKey, + PrivateKey = keys.PrivateKey, + Premium = premium, + ApiKey = "7gp59kKHt9kMlks0BuNC4IjNXYkljR", + + Kdf = KdfType.PBKDF2_SHA256, + KdfIterations = 5_000, + }; + + user.MasterPassword = passwordHasher.HashPassword(user, keys.MasterPasswordHash); + + return user; + } + + public static User CreateUserNoMangle(string email) { return new User { @@ -25,28 +71,35 @@ public class UserSeeder }; } - public static (User user, string userKey) CreateSdkUser(IPasswordHasher passwordHasher, string email) + public Dictionary GetMangleMap(User user, UserData expectedUserData) { - var nativeService = RustSdkServiceFactory.CreateSingleton(); - var keys = nativeService.GenerateUserKeys(email, "asdfasdfasdf"); - - var user = new User + var mangleMap = new Dictionary { - Id = Guid.NewGuid(), - Email = email, - MasterPassword = null, - SecurityStamp = "4830e359-e150-4eae-be2a-996c81c5e609", - Key = keys.EncryptedUserKey, - PublicKey = keys.PublicKey, - PrivateKey = keys.PrivateKey, - ApiKey = "7gp59kKHt9kMlks0BuNC4IjNXYkljR", - - Kdf = KdfType.PBKDF2_SHA256, - KdfIterations = 5_000, + { expectedUserData.Email, MangleEmail(expectedUserData.Email) }, + { expectedUserData.Id.ToString(), user.Id.ToString() }, + { expectedUserData.Kdf.ToString(), user.Kdf.ToString() }, + { expectedUserData.KdfIterations.ToString(CultureInfo.InvariantCulture), user.KdfIterations.ToString(CultureInfo.InvariantCulture) } }; + if (expectedUserData.Key != null) + { + mangleMap[expectedUserData.Key] = user.Key; + } - user.MasterPassword = passwordHasher.HashPassword(user, keys.MasterPasswordHash); + if (expectedUserData.PublicKey != null) + { + mangleMap[expectedUserData.PublicKey] = user.PublicKey; + } - return (user, keys.Key); + if (expectedUserData.PrivateKey != null) + { + mangleMap[expectedUserData.PrivateKey] = user.PrivateKey; + } + + if (expectedUserData.ApiKey != null) + { + mangleMap[expectedUserData.ApiKey] = user.ApiKey; + } + + return mangleMap; } } diff --git a/util/Seeder/IQuery.cs b/util/Seeder/IQuery.cs new file mode 100644 index 0000000000..adbcd8e59d --- /dev/null +++ b/util/Seeder/IQuery.cs @@ -0,0 +1,60 @@ +namespace Bit.Seeder; + +/// +/// Base interface for query operations in the seeding system. The base interface should not be used directly, rather use `IQuery<TRequest, TResult>`. +/// +/// +/// Queries are synchronous, read-only operations that retrieve data from the seeding context. +/// Unlike scenes which create data, queries fetch existing data based on request parameters. +/// They follow a type-safe pattern using generics to ensure proper request/response handling +/// while maintaining a common non-generic interface for dynamic invocation. +/// +public interface IQuery +{ + /// + /// Gets the type of request this query expects. + /// + /// The request type that this query can process. + Type GetRequestType(); + + /// + /// Executes the query based on the provided request object. + /// + /// The request object containing parameters for the query operation. + /// The query result data as an object. + object Execute(object request); +} + +/// +/// Generic query interface for synchronous, read-only operations with specific request and result types. +/// +/// The type of request object this query accepts. +/// The type of data this query returns. +/// +/// Use this interface when you need to retrieve existing data from the seeding context based on +/// specific request parameters. Queries are synchronous and do not modify data - they only read +/// and return information. The explicit interface implementations allow dynamic invocation while +/// maintaining type safety in the implementation. +/// +public interface IQuery : IQuery where TRequest : class where TResult : class +{ + /// + /// Executes the query based on the provided strongly-typed request and returns typed result data. + /// + /// The request object containing parameters for the query operation. + /// The typed query result data. + TResult Execute(TRequest request); + + /// + /// Gets the request type for this query. + /// + /// The type of TRequest. + Type IQuery.GetRequestType() => typeof(TRequest); + + /// + /// Adapts the non-generic Execute to the strongly-typed version. + /// + /// The request object to cast and process. + /// The typed result cast to object. + object IQuery.Execute(object request) => Execute((TRequest)request); +} diff --git a/util/Seeder/IScene.cs b/util/Seeder/IScene.cs new file mode 100644 index 0000000000..6f513973ba --- /dev/null +++ b/util/Seeder/IScene.cs @@ -0,0 +1,96 @@ +namespace Bit.Seeder; + +/// +/// Base interface for seeding operations. The base interface should not be used directly, rather use `IScene<Request>`. +/// +/// +/// Scenes are components in the seeding system that create and configure test data. They follow +/// a type-safe pattern using generics to ensure proper request/response handling while maintaining +/// a common non-generic interface for dynamic invocation. +/// +public interface IScene +{ + /// + /// Gets the type of request this scene expects. + /// + /// The request type that this scene can process. + Type GetRequestType(); + + /// + /// Seeds data based on the provided request object. + /// + /// The request object containing parameters for the seeding operation. + /// A scene result containing any returned data, mangle map, and entity tracking information. + Task> SeedAsync(object request); +} + +/// +/// Generic scene interface for seeding operations with a specific request type. Does not return a value beyond tracking entities and a mangle map. +/// +/// The type of request object this scene accepts. +/// +/// Use this interface when your scene needs to process a specific request type but doesn't need to +/// return any data beyond the standard mangle map for ID transformations and entity tracking. +/// The explicit interface implementations allow this scene to be invoked dynamically through the +/// base IScene interface while maintaining type safety in the implementation. +/// +public interface IScene : IScene where TRequest : class +{ + /// + /// Seeds data based on the provided strongly-typed request. + /// + /// The request object containing parameters for the seeding operation. + /// A scene result containing the mangle map and entity tracking information. + Task SeedAsync(TRequest request); + + /// + /// Gets the request type for this scene. + /// + /// The type of TRequest. + Type IScene.GetRequestType() => typeof(TRequest); + + /// + /// Adapts the non-generic SeedAsync to the strongly-typed version. + /// + /// The request object to cast and process. + /// A scene result wrapped as an object result. + async Task> IScene.SeedAsync(object request) + { + var result = await SeedAsync((TRequest)request); + return new SceneResult(mangleMap: result.MangleMap); + } +} + +/// +/// Generic scene interface for seeding operations with a specific request type that returns typed data. +/// +/// The type of request object this scene accepts. Must be a reference type. +/// The type of data this scene returns. Must be a reference type. +/// +/// Use this interface when your scene needs to return specific data that can be used by subsequent +/// scenes or test logic. The result is wrapped in a SceneResult that also includes the mangle map +/// and entity tracking information. The explicit interface implementations allow dynamic invocation +/// while preserving type safety in the implementation. +/// +public interface IScene : IScene where TRequest : class where TResult : class +{ + /// + /// Seeds data based on the provided strongly-typed request and returns typed result data. + /// + /// The request object containing parameters for the seeding operation. + /// A scene result containing the typed result data, mangle map, and entity tracking information. + Task> SeedAsync(TRequest request); + + /// + /// Gets the request type for this scene. + /// + /// The type of TRequest. + Type IScene.GetRequestType() => typeof(TRequest); + + /// + /// Adapts the non-generic SeedAsync to the strongly-typed version. + /// + /// The request object to cast and process. + /// A scene result with the typed result cast to object. + async Task> IScene.SeedAsync(object request) => (SceneResult)await SeedAsync((TRequest)request); +} diff --git a/util/Seeder/MangleId.cs b/util/Seeder/MangleId.cs new file mode 100644 index 0000000000..8cc862c28c --- /dev/null +++ b/util/Seeder/MangleId.cs @@ -0,0 +1,21 @@ +using System.Globalization; + +namespace Bit.Seeder; + +/// +/// Helper for generating unique identifier suffixes to prevent collisions in test data. +/// "Mangling" adds a random suffix to test data identifiers (usernames, emails, org names, etc.) +/// to ensure uniqueness across multiple test runs and parallel test executions. +/// +public class MangleId +{ + public readonly string Value; + + public MangleId() + { + // Generate a short random string (6 char) to use as the mangle ID + Value = Random.Shared.NextInt64().ToString("x", CultureInfo.InvariantCulture).Substring(0, 8); + } + + public override string ToString() => Value; +} diff --git a/util/Seeder/Queries/EmergencyAccessInviteQuery.cs b/util/Seeder/Queries/EmergencyAccessInviteQuery.cs new file mode 100644 index 0000000000..95d96a9a50 --- /dev/null +++ b/util/Seeder/Queries/EmergencyAccessInviteQuery.cs @@ -0,0 +1,35 @@ +using System.ComponentModel.DataAnnotations; +using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Tokens; +using Bit.Infrastructure.EntityFramework.Repositories; + +namespace Bit.Seeder.Queries; + +/// +/// Retrieves all emergency access invite urls for the provided email. +/// +public class EmergencyAccessInviteQuery( + DatabaseContext db, + IDataProtectorTokenFactory dataProtectorTokenizer) + : IQuery> +{ + public class Request + { + [Required] + public required string Email { get; set; } + } + + public IEnumerable Execute(Request request) + { + var invites = db.EmergencyAccesses + .Where(ea => ea.Email == request.Email).ToList().Select(ea => + { + var token = dataProtectorTokenizer.Protect( + new EmergencyAccessInviteTokenable(ea, hoursTillExpiration: 1) + ); + return $"/accept-emergency?id={ea.Id}&name=Dummy&email={ea.Email}&token={token}"; + }); + + return invites; + } +} diff --git a/util/Seeder/Recipes/OrganizationWithUsersRecipe.cs b/util/Seeder/Recipes/OrganizationWithUsersRecipe.cs index 7678c3a9ce..87fcc1967b 100644 --- a/util/Seeder/Recipes/OrganizationWithUsersRecipe.cs +++ b/util/Seeder/Recipes/OrganizationWithUsersRecipe.cs @@ -1,5 +1,5 @@ -using Bit.Core.Enums; -using Bit.Infrastructure.EntityFramework.Models; +using Bit.Core.Entities; +using Bit.Core.Enums; using Bit.Infrastructure.EntityFramework.Repositories; using Bit.Seeder.Factories; using LinqToDB.EntityFrameworkCore; @@ -12,14 +12,14 @@ public class OrganizationWithUsersRecipe(DatabaseContext db) { var seats = Math.Max(users + 1, 1000); var organization = OrganizationSeeder.CreateEnterprise(name, domain, seats); - var ownerUser = UserSeeder.CreateUser($"owner@{domain}"); + var ownerUser = UserSeeder.CreateUserNoMangle($"owner@{domain}"); var ownerOrgUser = organization.CreateOrganizationUser(ownerUser, OrganizationUserType.Owner, OrganizationUserStatusType.Confirmed); var additionalUsers = new List(); var additionalOrgUsers = new List(); for (var i = 0; i < users; i++) { - var additionalUser = UserSeeder.CreateUser($"user{i}@{domain}"); + var additionalUser = UserSeeder.CreateUserNoMangle($"user{i}@{domain}"); additionalUsers.Add(additionalUser); additionalOrgUsers.Add(organization.CreateOrganizationUser(additionalUser, OrganizationUserType.User, usersStatus)); } diff --git a/util/Seeder/SceneResult.cs b/util/Seeder/SceneResult.cs new file mode 100644 index 0000000000..7ac004f55e --- /dev/null +++ b/util/Seeder/SceneResult.cs @@ -0,0 +1,28 @@ +namespace Bit.Seeder; + +/// +/// Helper for exposing a interface with a SeedAsync method. +/// +public class SceneResult(Dictionary mangleMap) + : SceneResult(result: null, mangleMap: mangleMap); + +/// +/// Generic result from executing a Scene. +/// Contains custom scene-specific data and a mangle map that maps magic strings from the +/// request to their mangled (collision-free) values inserted into the database. +/// +/// The type of custom result data returned by the scene. +public class SceneResult(TResult result, Dictionary mangleMap) +{ + public TResult Result { get; init; } = result; + public Dictionary MangleMap { get; init; } = mangleMap; + + public static explicit operator SceneResult(SceneResult v) + { + var result = v.Result; + + return result is null + ? new SceneResult(result: null, mangleMap: v.MangleMap) + : new SceneResult(result: result, mangleMap: v.MangleMap); + } +} diff --git a/util/Seeder/Scenes/SingleUserScene.cs b/util/Seeder/Scenes/SingleUserScene.cs new file mode 100644 index 0000000000..df941c7f59 --- /dev/null +++ b/util/Seeder/Scenes/SingleUserScene.cs @@ -0,0 +1,38 @@ +using System.ComponentModel.DataAnnotations; +using Bit.Core.Repositories; +using Bit.Seeder.Factories; + +namespace Bit.Seeder.Scenes; + +/// +/// Creates a single user using the provided account details. +/// +public class SingleUserScene(UserSeeder userSeeder, IUserRepository userRepository) : IScene +{ + public class Request + { + [Required] + public required string Email { get; set; } + public bool EmailVerified { get; set; } = false; + public bool Premium { get; set; } = false; + } + + public async Task SeedAsync(Request request) + { + var user = userSeeder.CreateUser(request.Email, request.EmailVerified, request.Premium); + + await userRepository.CreateAsync(user); + + return new SceneResult(mangleMap: userSeeder.GetMangleMap(user, new UserData + { + Email = request.Email, + Id = user.Id, + Key = user.Key, + PublicKey = user.PublicKey, + PrivateKey = user.PrivateKey, + ApiKey = user.ApiKey, + Kdf = user.Kdf, + KdfIterations = user.KdfIterations, + })); + } +} diff --git a/util/Seeder/Seeder.csproj b/util/Seeder/Seeder.csproj index 4d7fbab767..fd6e26c1ee 100644 --- a/util/Seeder/Seeder.csproj +++ b/util/Seeder/Seeder.csproj @@ -12,10 +12,6 @@ false - - - - diff --git a/util/SeederApi/Commands/DestroyBatchScenesCommand.cs b/util/SeederApi/Commands/DestroyBatchScenesCommand.cs new file mode 100644 index 0000000000..50f6142a98 --- /dev/null +++ b/util/SeederApi/Commands/DestroyBatchScenesCommand.cs @@ -0,0 +1,36 @@ +using Bit.SeederApi.Commands.Interfaces; + +namespace Bit.SeederApi.Commands; + +public class DestroyBatchScenesCommand( + ILogger logger, + IDestroySceneCommand destroySceneCommand) : IDestroyBatchScenesCommand +{ + public async Task DestroyAsync(IEnumerable playIds) + { + var exceptions = new List(); + + var deleteTasks = playIds.Select(async playId => + { + try + { + await destroySceneCommand.DestroyAsync(playId); + } + catch (Exception ex) + { + lock (exceptions) + { + exceptions.Add(ex); + } + logger.LogError(ex, "Error deleting seeded data: {PlayId}", playId); + } + }); + + await Task.WhenAll(deleteTasks); + + if (exceptions.Count > 0) + { + throw new AggregateException("One or more errors occurred while deleting seeded data", exceptions); + } + } +} diff --git a/util/SeederApi/Commands/DestroySceneCommand.cs b/util/SeederApi/Commands/DestroySceneCommand.cs new file mode 100644 index 0000000000..0e0f4edd6d --- /dev/null +++ b/util/SeederApi/Commands/DestroySceneCommand.cs @@ -0,0 +1,57 @@ +using Bit.Core.Repositories; +using Bit.Infrastructure.EntityFramework.Repositories; +using Bit.SeederApi.Commands.Interfaces; +using Bit.SeederApi.Services; + +namespace Bit.SeederApi.Commands; + +public class DestroySceneCommand( + DatabaseContext databaseContext, + ILogger logger, + IUserRepository userRepository, + IPlayItemRepository playItemRepository, + IOrganizationRepository organizationRepository) : IDestroySceneCommand +{ + public async Task DestroyAsync(string playId) + { + // Note, delete cascade will remove PlayItem entries + + var playItem = await playItemRepository.GetByPlayIdAsync(playId); + var userIds = playItem.Select(pd => pd.UserId).Distinct().ToList(); + var organizationIds = playItem.Select(pd => pd.OrganizationId).Distinct().ToList(); + + // Delete Users before Organizations to respect foreign key constraints + if (userIds.Count > 0) + { + var users = databaseContext.Users.Where(u => userIds.Contains(u.Id)); + await userRepository.DeleteManyAsync(users); + } + + if (organizationIds.Count > 0) + { + var organizations = databaseContext.Organizations.Where(o => organizationIds.Contains(o.Id)); + var aggregateException = new AggregateException(); + foreach (var org in organizations) + { + try + { + await organizationRepository.DeleteAsync(org); + } + catch (Exception ex) + { + aggregateException = new AggregateException(aggregateException, ex); + } + } + if (aggregateException.InnerExceptions.Count > 0) + { + throw new SceneExecutionException( + $"One or more errors occurred while deleting organizations for seed ID {playId}", + aggregateException); + } + } + + logger.LogInformation("Successfully destroyed seeded data with ID {PlayId}", playId); + + return new { PlayId = playId }; + } +} diff --git a/util/SeederApi/Commands/Interfaces/IDestroyBatchScenesCommand.cs b/util/SeederApi/Commands/Interfaces/IDestroyBatchScenesCommand.cs new file mode 100644 index 0000000000..ce43f26a54 --- /dev/null +++ b/util/SeederApi/Commands/Interfaces/IDestroyBatchScenesCommand.cs @@ -0,0 +1,14 @@ +namespace Bit.SeederApi.Commands.Interfaces; + +/// +/// Command for destroying multiple scenes in parallel. +/// +public interface IDestroyBatchScenesCommand +{ + /// + /// Destroys multiple scenes by their play IDs in parallel. + /// + /// The list of play IDs to destroy + /// Thrown when one or more scenes fail to destroy + Task DestroyAsync(IEnumerable playIds); +} diff --git a/util/SeederApi/Commands/Interfaces/IDestroySceneCommand.cs b/util/SeederApi/Commands/Interfaces/IDestroySceneCommand.cs new file mode 100644 index 0000000000..a3b0800bb2 --- /dev/null +++ b/util/SeederApi/Commands/Interfaces/IDestroySceneCommand.cs @@ -0,0 +1,15 @@ +namespace Bit.SeederApi.Commands.Interfaces; + +/// +/// Command for destroying data created by a single scene. +/// +public interface IDestroySceneCommand +{ + /// + /// Destroys data created by a scene using the seeded data ID. + /// + /// The ID of the seeded data to destroy + /// The result of the destroy operation + /// Thrown when there's an error destroying the seeded data + Task DestroyAsync(string playId); +} diff --git a/util/SeederApi/Controllers/InfoController.cs b/util/SeederApi/Controllers/InfoController.cs new file mode 100644 index 0000000000..de4a264ddb --- /dev/null +++ b/util/SeederApi/Controllers/InfoController.cs @@ -0,0 +1,20 @@ +using Bit.Core.Utilities; +using Microsoft.AspNetCore.Mvc; + +namespace Bit.SeederApi.Controllers; + +public class InfoController : Controller +{ + [HttpGet("~/alive")] + [HttpGet("~/now")] + public DateTime GetAlive() + { + return DateTime.UtcNow; + } + + [HttpGet("~/version")] + public JsonResult GetVersion() + { + return Json(AssemblyHelpers.GetVersion()); + } +} diff --git a/util/SeederApi/Controllers/QueryController.cs b/util/SeederApi/Controllers/QueryController.cs new file mode 100644 index 0000000000..22bf84e5b7 --- /dev/null +++ b/util/SeederApi/Controllers/QueryController.cs @@ -0,0 +1,32 @@ +using Bit.SeederApi.Execution; +using Bit.SeederApi.Models.Request; +using Bit.SeederApi.Services; +using Microsoft.AspNetCore.Mvc; + +namespace Bit.SeederApi.Controllers; + +[Route("query")] +public class QueryController(ILogger logger, IQueryExecutor queryExecutor) : Controller +{ + [HttpPost] + public IActionResult Query([FromBody] QueryRequestModel request) + { + logger.LogInformation("Executing query: {Query}", request.Template); + + try + { + var result = queryExecutor.Execute(request.Template, request.Arguments); + + return Json(result); + } + catch (QueryNotFoundException ex) + { + return NotFound(new { Error = ex.Message }); + } + catch (QueryExecutionException ex) + { + logger.LogError(ex, "Error executing query: {Query}", request.Template); + return BadRequest(new { Error = ex.Message, Details = ex.InnerException?.Message }); + } + } +} diff --git a/util/SeederApi/Controllers/SeedController.cs b/util/SeederApi/Controllers/SeedController.cs new file mode 100644 index 0000000000..44f0dbaf2c --- /dev/null +++ b/util/SeederApi/Controllers/SeedController.cs @@ -0,0 +1,100 @@ +using Bit.SeederApi.Commands.Interfaces; +using Bit.SeederApi.Execution; +using Bit.SeederApi.Models.Request; +using Bit.SeederApi.Queries.Interfaces; +using Bit.SeederApi.Services; +using Microsoft.AspNetCore.Mvc; + +namespace Bit.SeederApi.Controllers; + +[Route("seed")] +public class SeedController( + ILogger logger, + ISceneExecutor sceneExecutor, + IDestroySceneCommand destroySceneCommand, + IDestroyBatchScenesCommand destroyBatchScenesCommand, + IGetAllPlayIdsQuery getAllPlayIdsQuery) : Controller +{ + [HttpPost] + public async Task SeedAsync([FromBody] SeedRequestModel request) + { + logger.LogInformation("Received seed request with template: {Template}", request.Template); + + try + { + var response = await sceneExecutor.ExecuteAsync(request.Template, request.Arguments); + + return Json(response); + } + catch (SceneNotFoundException ex) + { + return NotFound(new { Error = ex.Message }); + } + catch (SceneExecutionException ex) + { + logger.LogError(ex, "Error executing scene: {Template}", request.Template); + return BadRequest(new { Error = ex.Message, Details = ex.InnerException?.Message }); + } + } + + [HttpDelete("batch")] + public async Task DeleteBatchAsync([FromBody] List playIds) + { + logger.LogInformation("Deleting batch of seeded data with IDs: {PlayIds}", string.Join(", ", playIds)); + + try + { + await destroyBatchScenesCommand.DestroyAsync(playIds); + return Ok(new { Message = "Batch delete completed successfully" }); + } + catch (AggregateException ex) + { + return BadRequest(new + { + Error = ex.Message, + Details = ex.InnerExceptions.Select(e => e.Message).ToList() + }); + } + } + + [HttpDelete("{playId}")] + public async Task DeleteAsync([FromRoute] string playId) + { + logger.LogInformation("Deleting seeded data with ID: {PlayId}", playId); + + try + { + var result = await destroySceneCommand.DestroyAsync(playId); + + return Json(result); + } + catch (SceneExecutionException ex) + { + logger.LogError(ex, "Error deleting seeded data: {PlayId}", playId); + return BadRequest(new { Error = ex.Message, Details = ex.InnerException?.Message }); + } + } + + + [HttpDelete] + public async Task DeleteAllAsync() + { + logger.LogInformation("Deleting all seeded data"); + + var playIds = getAllPlayIdsQuery.GetAllPlayIds(); + + try + { + await destroyBatchScenesCommand.DestroyAsync(playIds); + return NoContent(); + } + catch (AggregateException ex) + { + return BadRequest(new + { + Error = ex.Message, + Details = ex.InnerExceptions.Select(e => e.Message).ToList() + }); + } + } +} diff --git a/util/SeederApi/Execution/IQueryExecutor.cs b/util/SeederApi/Execution/IQueryExecutor.cs new file mode 100644 index 0000000000..ebd971bbb7 --- /dev/null +++ b/util/SeederApi/Execution/IQueryExecutor.cs @@ -0,0 +1,22 @@ +using System.Text.Json; + +namespace Bit.SeederApi.Execution; + +/// +/// Executor for dynamically resolving and executing queries by name. +/// This is an infrastructure component that orchestrates query execution, +/// not a domain-level query. +/// +public interface IQueryExecutor +{ + /// + /// Executes a query with the given query name and arguments. + /// Queries are read-only and do not track entities or create seed IDs. + /// + /// The name of the query (e.g., "EmergencyAccessInviteQuery") + /// Optional JSON arguments to pass to the query's Execute method + /// The result of the query execution + /// Thrown when the query is not found + /// Thrown when there's an error executing the query + object Execute(string queryName, JsonElement? arguments); +} diff --git a/util/SeederApi/Execution/ISceneExecutor.cs b/util/SeederApi/Execution/ISceneExecutor.cs new file mode 100644 index 0000000000..f15909ea79 --- /dev/null +++ b/util/SeederApi/Execution/ISceneExecutor.cs @@ -0,0 +1,22 @@ +using System.Text.Json; +using Bit.SeederApi.Models.Response; + +namespace Bit.SeederApi.Execution; + +/// +/// Executor for dynamically resolving and executing scenes by template name. +/// This is an infrastructure component that orchestrates scene execution, +/// not a domain-level command. +/// +public interface ISceneExecutor +{ + /// + /// Executes a scene with the given template name and arguments. + /// + /// The name of the scene template (e.g., "SingleUserScene") + /// Optional JSON arguments to pass to the scene's Seed method + /// A scene response model containing the result and mangle map + /// Thrown when the scene template is not found + /// Thrown when there's an error executing the scene + Task ExecuteAsync(string templateName, JsonElement? arguments); +} diff --git a/util/SeederApi/Execution/JsonConfiguration.cs b/util/SeederApi/Execution/JsonConfiguration.cs new file mode 100644 index 0000000000..beef36e62a --- /dev/null +++ b/util/SeederApi/Execution/JsonConfiguration.cs @@ -0,0 +1,19 @@ +using System.Text.Json; + +namespace Bit.SeederApi.Execution; + +/// +/// Provides shared JSON serialization configuration for executors. +/// +internal static class JsonConfiguration +{ + /// + /// Standard JSON serializer options used for deserializing scene and query request models. + /// Uses case-insensitive property matching and camelCase naming policy. + /// + internal static readonly JsonSerializerOptions Options = new() + { + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; +} diff --git a/util/SeederApi/Execution/QueryExecutor.cs b/util/SeederApi/Execution/QueryExecutor.cs new file mode 100644 index 0000000000..5473586c22 --- /dev/null +++ b/util/SeederApi/Execution/QueryExecutor.cs @@ -0,0 +1,77 @@ +using System.Text.Json; +using Bit.Seeder; +using Bit.SeederApi.Services; + +namespace Bit.SeederApi.Execution; + +public class QueryExecutor( + ILogger logger, + IServiceProvider serviceProvider) : IQueryExecutor +{ + + public object Execute(string queryName, JsonElement? arguments) + { + try + { + var query = serviceProvider.GetKeyedService(queryName) + ?? throw new QueryNotFoundException(queryName); + + var requestType = query.GetRequestType(); + var requestModel = DeserializeRequestModel(queryName, requestType, arguments); + var result = query.Execute(requestModel); + + logger.LogInformation("Successfully executed query: {QueryName}", queryName); + return result; + } + catch (Exception ex) when (ex is not QueryNotFoundException and not QueryExecutionException) + { + logger.LogError(ex, "Unexpected error executing query: {QueryName}", queryName); + throw new QueryExecutionException( + $"An unexpected error occurred while executing query '{queryName}'", + ex.InnerException ?? ex); + } + } + + private object DeserializeRequestModel(string queryName, Type requestType, JsonElement? arguments) + { + if (arguments == null) + { + return CreateDefaultRequestModel(queryName, requestType); + } + + try + { + var requestModel = JsonSerializer.Deserialize(arguments.Value.GetRawText(), requestType, JsonConfiguration.Options); + if (requestModel == null) + { + throw new QueryExecutionException( + $"Failed to deserialize request model for query '{queryName}'"); + } + return requestModel; + } + catch (JsonException ex) + { + throw new QueryExecutionException( + $"Failed to deserialize request model for query '{queryName}': {ex.Message}", ex); + } + } + + private object CreateDefaultRequestModel(string queryName, Type requestType) + { + try + { + var requestModel = Activator.CreateInstance(requestType); + if (requestModel == null) + { + throw new QueryExecutionException( + $"Arguments are required for query '{queryName}'"); + } + return requestModel; + } + catch + { + throw new QueryExecutionException( + $"Arguments are required for query '{queryName}'"); + } + } +} diff --git a/util/SeederApi/Execution/SceneExecutor.cs b/util/SeederApi/Execution/SceneExecutor.cs new file mode 100644 index 0000000000..f31dd7d943 --- /dev/null +++ b/util/SeederApi/Execution/SceneExecutor.cs @@ -0,0 +1,78 @@ +using System.Text.Json; +using Bit.Seeder; +using Bit.SeederApi.Models.Response; +using Bit.SeederApi.Services; + +namespace Bit.SeederApi.Execution; + +public class SceneExecutor( + ILogger logger, + IServiceProvider serviceProvider) : ISceneExecutor +{ + + public async Task ExecuteAsync(string templateName, JsonElement? arguments) + { + try + { + var scene = serviceProvider.GetKeyedService(templateName) + ?? throw new SceneNotFoundException(templateName); + + var requestType = scene.GetRequestType(); + var requestModel = DeserializeRequestModel(templateName, requestType, arguments); + var result = await scene.SeedAsync(requestModel); + + logger.LogInformation("Successfully executed scene: {TemplateName}", templateName); + return SceneResponseModel.FromSceneResult(result); + } + catch (Exception ex) when (ex is not SceneNotFoundException and not SceneExecutionException) + { + logger.LogError(ex, "Unexpected error executing scene: {TemplateName}", templateName); + throw new SceneExecutionException( + $"An unexpected error occurred while executing scene '{templateName}'", + ex.InnerException ?? ex); + } + } + + private object DeserializeRequestModel(string templateName, Type requestType, JsonElement? arguments) + { + if (arguments == null) + { + return CreateDefaultRequestModel(templateName, requestType); + } + + try + { + var requestModel = JsonSerializer.Deserialize(arguments.Value.GetRawText(), requestType, JsonConfiguration.Options); + if (requestModel == null) + { + throw new SceneExecutionException( + $"Failed to deserialize request model for scene '{templateName}'"); + } + return requestModel; + } + catch (JsonException ex) + { + throw new SceneExecutionException( + $"Failed to deserialize request model for scene '{templateName}': {ex.Message}", ex); + } + } + + private object CreateDefaultRequestModel(string templateName, Type requestType) + { + try + { + var requestModel = Activator.CreateInstance(requestType); + if (requestModel == null) + { + throw new SceneExecutionException( + $"Arguments are required for scene '{templateName}'"); + } + return requestModel; + } + catch + { + throw new SceneExecutionException( + $"Arguments are required for scene '{templateName}'"); + } + } +} diff --git a/util/SeederApi/Extensions/ServiceCollectionExtensions.cs b/util/SeederApi/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000000..052da28dfc --- /dev/null +++ b/util/SeederApi/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,76 @@ +using System.Reflection; +using Bit.Seeder; +using Bit.SeederApi.Commands; +using Bit.SeederApi.Commands.Interfaces; +using Bit.SeederApi.Execution; +using Bit.SeederApi.Queries; +using Bit.SeederApi.Queries.Interfaces; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Bit.SeederApi.Extensions; + +public static class ServiceCollectionExtensions +{ + /// + /// Registers SeederApi executors, commands, and queries. + /// + public static IServiceCollection AddSeederApiServices(this IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + + services.AddScoped(); + services.AddScoped(); + + services.AddScoped(); + + return services; + } + + /// + /// Dynamically registers all scene types that implement IScene<TRequest> from the Seeder assembly. + /// Scenes are registered as keyed scoped services using their class name as the key. + /// + public static IServiceCollection AddScenes(this IServiceCollection services) + { + var iSceneType1 = typeof(IScene<>); + var iSceneType2 = typeof(IScene<,>); + var isIScene = (Type t) => t == iSceneType1 || t == iSceneType2; + + var seederAssembly = Assembly.Load("Seeder"); + var sceneTypes = seederAssembly.GetTypes() + .Where(t => t is { IsClass: true, IsAbstract: false } && + t.GetInterfaces().Any(i => i.IsGenericType && + isIScene(i.GetGenericTypeDefinition()))); + + foreach (var sceneType in sceneTypes) + { + services.TryAddScoped(sceneType); + services.TryAddKeyedScoped(typeof(IScene), sceneType.Name, (sp, _) => sp.GetRequiredService(sceneType)); + } + + return services; + } + + /// + /// Dynamically registers all query types that implement IQuery<TRequest> from the Seeder assembly. + /// Queries are registered as keyed scoped services using their class name as the key. + /// + public static IServiceCollection AddQueries(this IServiceCollection services) + { + var iQueryType = typeof(IQuery<,>); + var seederAssembly = Assembly.Load("Seeder"); + var queryTypes = seederAssembly.GetTypes() + .Where(t => t is { IsClass: true, IsAbstract: false } && + t.GetInterfaces().Any(i => i.IsGenericType && + i.GetGenericTypeDefinition() == iQueryType)); + + foreach (var queryType in queryTypes) + { + services.TryAddScoped(queryType); + services.TryAddKeyedScoped(typeof(IQuery), queryType.Name, (sp, _) => sp.GetRequiredService(queryType)); + } + + return services; + } +} diff --git a/util/SeederApi/Models/Request/QueryRequestModel.cs b/util/SeederApi/Models/Request/QueryRequestModel.cs new file mode 100644 index 0000000000..38751bc21b --- /dev/null +++ b/util/SeederApi/Models/Request/QueryRequestModel.cs @@ -0,0 +1,11 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json; + +namespace Bit.SeederApi.Models.Request; + +public class QueryRequestModel +{ + [Required] + public required string Template { get; set; } + public JsonElement? Arguments { get; set; } +} diff --git a/util/SeederApi/Models/Request/SeedRequestModel.cs b/util/SeederApi/Models/Request/SeedRequestModel.cs new file mode 100644 index 0000000000..404af97ebe --- /dev/null +++ b/util/SeederApi/Models/Request/SeedRequestModel.cs @@ -0,0 +1,11 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json; + +namespace Bit.SeederApi.Models.Request; + +public class SeedRequestModel +{ + [Required] + public required string Template { get; set; } + public JsonElement? Arguments { get; set; } +} diff --git a/util/SeederApi/Models/Response/SeedResponseModel.cs b/util/SeederApi/Models/Response/SeedResponseModel.cs new file mode 100644 index 0000000000..a8913c76c8 --- /dev/null +++ b/util/SeederApi/Models/Response/SeedResponseModel.cs @@ -0,0 +1,18 @@ +using Bit.Seeder; + +namespace Bit.SeederApi.Models.Response; + +public class SceneResponseModel +{ + public required Dictionary? MangleMap { get; init; } + public required object? Result { get; init; } + + public static SceneResponseModel FromSceneResult(SceneResult sceneResult) + { + return new SceneResponseModel + { + Result = sceneResult.Result, + MangleMap = sceneResult.MangleMap, + }; + } +} diff --git a/util/SeederApi/Program.cs b/util/SeederApi/Program.cs new file mode 100644 index 0000000000..2067df307a --- /dev/null +++ b/util/SeederApi/Program.cs @@ -0,0 +1,20 @@ +using Bit.Core.Utilities; + +namespace Bit.SeederApi; + +public class Program +{ + public static void Main(string[] args) + { + Host + .CreateDefaultBuilder(args) + .ConfigureCustomAppConfiguration(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }) + .AddSerilogFileLogging() + .Build() + .Run(); + } +} diff --git a/util/SeederApi/Properties/launchSettings.json b/util/SeederApi/Properties/launchSettings.json new file mode 100644 index 0000000000..95cd77e255 --- /dev/null +++ b/util/SeederApi/Properties/launchSettings.json @@ -0,0 +1,37 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:5047", + "sslPort": 0 + } + }, + "profiles": { + "SeederApi": { + "commandName": "Project", + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5047", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "SeederApi-SelfHost": { + "commandName": "Project", + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5048", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "developSelfHosted": "true" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/util/SeederApi/Queries/GetAllPlayIdsQuery.cs b/util/SeederApi/Queries/GetAllPlayIdsQuery.cs new file mode 100644 index 0000000000..7bc72e5b07 --- /dev/null +++ b/util/SeederApi/Queries/GetAllPlayIdsQuery.cs @@ -0,0 +1,15 @@ +using Bit.Infrastructure.EntityFramework.Repositories; +using Bit.SeederApi.Queries.Interfaces; + +namespace Bit.SeederApi.Queries; + +public class GetAllPlayIdsQuery(DatabaseContext databaseContext) : IGetAllPlayIdsQuery +{ + public List GetAllPlayIds() + { + return databaseContext.PlayItem + .Select(pd => pd.PlayId) + .Distinct() + .ToList(); + } +} diff --git a/util/SeederApi/Queries/Interfaces/IGetAllPlayIdsQuery.cs b/util/SeederApi/Queries/Interfaces/IGetAllPlayIdsQuery.cs new file mode 100644 index 0000000000..ea9c44991a --- /dev/null +++ b/util/SeederApi/Queries/Interfaces/IGetAllPlayIdsQuery.cs @@ -0,0 +1,13 @@ +namespace Bit.SeederApi.Queries.Interfaces; + +/// +/// Query for retrieving all play IDs for currently tracked seeded data. +/// +public interface IGetAllPlayIdsQuery +{ + /// + /// Retrieves all play IDs for currently tracked seeded data. + /// + /// A list of play IDs representing active seeded data that can be destroyed. + List GetAllPlayIds(); +} diff --git a/util/SeederApi/README.md b/util/SeederApi/README.md new file mode 100644 index 0000000000..a5a5d4ab9f --- /dev/null +++ b/util/SeederApi/README.md @@ -0,0 +1,185 @@ +# SeederApi + +A web API for dynamically seeding and querying test data in the Bitwarden database during development and testing. + +## Overview + +The SeederApi provides HTTP endpoints to execute [Seeder](../Seeder/README.md) scenes and queries, enabling automated test data +generation and retrieval through a RESTful interface. This is particularly useful for integration testing, local +development workflows, and automated test environments. + +## Architecture + +The SeederApi consists of three main components: + +1. **Controllers** - HTTP endpoints for seeding, querying, and managing test data +2. **Services** - Business logic for scene and query execution +3. **Models** - Request/response models for API communication + +### Key Components + +- **SeedController** (`/seed`) - Creates and destroys seeded test data +- **QueryController** (`/query`) - Executes read-only queries against existing data +- **InfoController** (`/alive`, `/version`) - Health check and version information +- **SceneService** - Manages scene execution and cleanup with play ID tracking +- **QueryService** - Executes read-only query operations + +## How To Use + +### Starting the API + +```bash +cd util/SeederApi +dotnet run +``` + +The API will start on the configured port (typically `http://localhost:5000`). + +### Seeding Data + +Send a POST request to `/seed` with a scene template name and optional arguments. Include the `X-Play-Id` header to +track the seeded data for later cleanup: + +```bash +curl -X POST http://localhost:5000/seed \ + -H "Content-Type: application/json" \ + -H "X-Play-Id: test-run-123" \ + -d '{ + "template": "SingleUserScene", + "arguments": { + "email": "test@example.com" + } + }' +``` + +**Response:** + +```json +{ + "mangleMap": { + "test@example.com": "1854b016+test@example.com", + "42bcf05d-7ad0-4e27-8b53-b3b700acc664": "42bcf05d-7ad0-4e27-8b53-b3b700acc664" + }, + "result": null +} +``` + +The `result` contains the data returned by the scene, and `mangleMap` contains ID mappings if ID mangling is enabled. +Use the `X-Play-Id` header value to later destroy the seeded data. + +### Querying Data + +Send a POST request to `/query` to execute read-only queries: + +```bash +curl -X POST http://localhost:5000/query \ + -H "Content-Type: application/json" \ + -d '{ + "template": "EmergencyAccessInviteQuery", + "arguments": { + "email": "test@example.com" + } + }' +``` + +**Response:** + +```json +["/accept-emergency?..."] +``` + +### Destroying Seeded Data + +#### Delete by Play ID + +Use the same play ID value you provided in the `X-Play-Id` header: + +```bash +curl -X DELETE http://localhost:5000/seed/test-run-123 +``` + +#### Delete Multiple by Play IDs + +```bash +curl -X DELETE http://localhost:5000/seed/batch \ + -H "Content-Type: application/json" \ + -d '["test-run-123", "test-run-456"]' +``` + +#### Delete All Seeded Data + +```bash +curl -X DELETE http://localhost:5000/seed +``` + +### Health Checks + +```bash +# Check if API is alive +curl http://localhost:5000/alive + +# Get API version +curl http://localhost:5000/version +``` + +## Creating Scenes and Queries + +Scenes and queries are defined in the [Seeder](../Seeder/README.md) project. The SeederApi automatically discovers and registers all +classes implementing the scene and query interfaces. + +## Configuration + +The SeederApi uses the standard Bitwarden configuration system: + +- `appsettings.json` - Base configuration +- `appsettings.Development.json` - Development overrides +- `dev/secrets.json` - Local secrets (database connection strings, etc.) +- User Secrets ID: `bitwarden-seeder-api` + +### Required Settings + +The SeederApi requires the following configuration: + +- **Database Connection** - Connection string to the Bitwarden database +- **Global Settings** - Standard Bitwarden `GlobalSettings` configuration + +## Play ID Tracking + +Certain entities such as Users and Organizations are tracked when created by a request including a PlayId. This enables +entities to be deleted after using the PlayId. + +### The X-Play-Id Header + +**Important:** All seed requests should include the `X-Play-Id` header: + +```bash +-H "X-Play-Id: your-unique-identifier" +``` + +The play ID can be any string that uniquely identifies your test run or session. Common patterns: + +### How Play ID Tracking Works + +When `TestPlayIdTrackingEnabled` is enabled in GlobalSettings, the `PlayIdMiddleware` +(see `src/SharedWeb/Utilities/PlayIdMiddleware.cs:7-23`) automatically: + +1. **Extracts** the `X-Play-Id` header from incoming requests +2. **Sets** the play ID in the `PlayIdService` for the request scope +3. **Tracks** all entities (users, organizations, etc.) created during the request +4. **Associates** them with the play ID in the `PlayItem` table +5. **Enables** complete cleanup via the delete endpoints + +This tracking works for **any API request** that includes the `X-Play-Id` header, not just SeederApi endpoints. This means +you can track entities created through: + +- **Scene executions** - Data seeded via `/seed` endpoint +- **Regular API operations** - Users signing up, creating organizations, inviting members, etc. +- **Integration tests** - Any HTTP requests to the Bitwarden API during test execution + +Without the `X-Play-Id` header, entities will not be tracked and cannot be cleaned up using the delete endpoints. + +## Security Considerations + +> [!WARNING] +> The SeederApi is intended for **development and testing environments only**. Never deploy this API to production +> environments. diff --git a/util/SeederApi/SeederApi.csproj b/util/SeederApi/SeederApi.csproj new file mode 100644 index 0000000000..53e9941c1c --- /dev/null +++ b/util/SeederApi/SeederApi.csproj @@ -0,0 +1,16 @@ + + + + bitwarden-seeder-api + net8.0 + enable + enable + false + + + + + + + + diff --git a/util/SeederApi/Services/QueryExceptions.cs b/util/SeederApi/Services/QueryExceptions.cs new file mode 100644 index 0000000000..beb0625cbb --- /dev/null +++ b/util/SeederApi/Services/QueryExceptions.cs @@ -0,0 +1,10 @@ +namespace Bit.SeederApi.Services; + +public class QueryNotFoundException(string query) : Exception($"Query '{query}' not found"); + +public class QueryExecutionException : Exception +{ + public QueryExecutionException(string message) : base(message) { } + public QueryExecutionException(string message, Exception innerException) + : base(message, innerException) { } +} diff --git a/util/SeederApi/Services/SceneExceptions.cs b/util/SeederApi/Services/SceneExceptions.cs new file mode 100644 index 0000000000..2d8da19629 --- /dev/null +++ b/util/SeederApi/Services/SceneExceptions.cs @@ -0,0 +1,10 @@ +namespace Bit.SeederApi.Services; + +public class SceneNotFoundException(string scene) : Exception($"Scene '{scene}' not found"); + +public class SceneExecutionException : Exception +{ + public SceneExecutionException(string message) : base(message) { } + public SceneExecutionException(string message, Exception innerException) + : base(message, innerException) { } +} diff --git a/util/SeederApi/Startup.cs b/util/SeederApi/Startup.cs new file mode 100644 index 0000000000..420078f509 --- /dev/null +++ b/util/SeederApi/Startup.cs @@ -0,0 +1,80 @@ +using System.Globalization; +using Bit.Core.Settings; +using Bit.Seeder; +using Bit.Seeder.Factories; +using Bit.SeederApi.Extensions; +using Bit.SharedWeb.Utilities; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Bit.SeederApi; + +public class Startup +{ + public Startup(IWebHostEnvironment env, IConfiguration configuration) + { + CultureInfo.DefaultThreadCurrentCulture = new CultureInfo("en-US"); + Configuration = configuration; + Environment = env; + } + + public IConfiguration Configuration { get; private set; } + public IWebHostEnvironment Environment { get; set; } + + public void ConfigureServices(IServiceCollection services) + { + services.AddOptions(); + + var globalSettings = services.AddGlobalSettingsServices(Configuration, Environment); + + services.AddCustomDataProtectionServices(Environment, globalSettings); + + services.AddTokenizers(); + services.AddDatabaseRepositories(globalSettings); + services.AddTestPlayIdTracking(globalSettings); + + services.TryAddSingleton(); + + services.AddScoped, PasswordHasher>(); + + services.AddSingleton(); + services.AddScoped(); + + services.AddSeederApiServices(); + + services.AddScoped(_ => new MangleId()); + services.AddScenes(); + services.AddQueries(); + + services.AddControllers(); + } + + public void Configure( + IApplicationBuilder app, + IWebHostEnvironment env, + IHostApplicationLifetime appLifetime, + GlobalSettings globalSettings) + { + if (env.IsProduction()) + { + throw new InvalidOperationException( + "SeederApi cannot be run in production environments. This service is intended for test data generation only."); + } + + if (globalSettings.TestPlayIdTrackingEnabled) + { + app.UseMiddleware(); + } + + if (!env.IsDevelopment()) + { + app.UseExceptionHandler("/Home/Error"); + } + + app.UseRouting(); + app.UseEndpoints(endpoints => + { + endpoints.MapControllerRoute(name: "default", pattern: "{controller=Seed}/{action=Index}/{id?}"); + }); + } +} diff --git a/util/SeederApi/appsettings.Development.json b/util/SeederApi/appsettings.Development.json new file mode 100644 index 0000000000..0c208ae918 --- /dev/null +++ b/util/SeederApi/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/util/SeederApi/appsettings.json b/util/SeederApi/appsettings.json new file mode 100644 index 0000000000..79388a1bb0 --- /dev/null +++ b/util/SeederApi/appsettings.json @@ -0,0 +1,11 @@ +{ + "globalSettings": { + "projectName": "SeederApi" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/util/Server/Program.cs b/util/Server/Program.cs index a2d7e5f687..3d563830ab 100644 --- a/util/Server/Program.cs +++ b/util/Server/Program.cs @@ -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; } } diff --git a/util/Server/Server.csproj b/util/Server/Server.csproj index 5aaeee7f4a..c64019e154 100644 --- a/util/Server/Server.csproj +++ b/util/Server/Server.csproj @@ -2,6 +2,8 @@ false + + $(WarningsNotAsErrors);CA1305 diff --git a/util/Setup/Setup.csproj b/util/Setup/Setup.csproj index 6366d46d3d..b4ab0bd806 100644 --- a/util/Setup/Setup.csproj +++ b/util/Setup/Setup.csproj @@ -3,6 +3,8 @@ Exe 1701;1702;1705;NU1701 + + $(WarningsNotAsErrors);CA1305 diff --git a/util/SqlServerEFScaffold/SqlServerEFScaffold.csproj b/util/SqlServerEFScaffold/SqlServerEFScaffold.csproj index 47001803ad..a2fb8173bf 100644 --- a/util/SqlServerEFScaffold/SqlServerEFScaffold.csproj +++ b/util/SqlServerEFScaffold/SqlServerEFScaffold.csproj @@ -1,4 +1,9 @@ + + + $(WarningsNotAsErrors);CA1305 + + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/util/SqliteMigrations/Migrations/20260108193841_CreatePlayItem.Designer.cs b/util/SqliteMigrations/Migrations/20260108193841_CreatePlayItem.Designer.cs new file mode 100644 index 0000000000..eac0b557a4 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20260108193841_CreatePlayItem.Designer.cs @@ -0,0 +1,3491 @@ +// +using System; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20260108193841_CreatePlayItem")] + partial class CreatePlayItem + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.8"); + + modelBuilder.Entity("Bit.Core.Dirt.Reports.Models.Data.OrganizationMemberBaseDetail", b => + { + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("CollectionName") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasColumnType("TEXT"); + + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("GroupName") + .HasColumnType("TEXT"); + + b.Property("HidePasswords") + .HasColumnType("INTEGER"); + + b.Property("Manage") + .HasColumnType("INTEGER"); + + b.Property("ReadOnly") + .HasColumnType("INTEGER"); + + b.Property("ResetPasswordKey") + .HasColumnType("TEXT"); + + b.Property("TwoFactorProviders") + .HasColumnType("TEXT"); + + b.Property("UserGuid") + .HasColumnType("TEXT"); + + b.Property("UserName") + .HasColumnType("TEXT"); + + b.Property("UsesKeyConnector") + .HasColumnType("INTEGER"); + + b.ToTable("OrganizationMemberBaseDetails"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LimitCollectionCreation") + .HasColumnType("INTEGER"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("INTEGER"); + + b.Property("LimitItemDeletion") + .HasColumnType("INTEGER"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("INTEGER"); + + b.Property("MaxAutoscaleSmSeats") + .HasColumnType("INTEGER"); + + b.Property("MaxAutoscaleSmServiceAccounts") + .HasColumnType("INTEGER"); + + b.Property("MaxCollections") + .HasColumnType("INTEGER"); + + b.Property("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("TEXT"); + + b.Property("Plan") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PlanType") + .HasColumnType("INTEGER"); + + b.Property("PrivateKey") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasColumnType("TEXT"); + + b.Property("ReferenceData") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Seats") + .HasColumnType("INTEGER"); + + b.Property("SelfHost") + .HasColumnType("INTEGER"); + + b.Property("SmSeats") + .HasColumnType("INTEGER"); + + b.Property("SmServiceAccounts") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Storage") + .HasColumnType("INTEGER"); + + b.Property("SyncSeats") + .HasColumnType("INTEGER"); + + b.Property("TwoFactorProviders") + .HasColumnType("TEXT"); + + b.Property("Use2fa") + .HasColumnType("INTEGER"); + + b.Property("UseAdminSponsoredFamilies") + .HasColumnType("INTEGER"); + + b.Property("UseApi") + .HasColumnType("INTEGER"); + + b.Property("UseAutomaticUserConfirmation") + .HasColumnType("INTEGER"); + + b.Property("UseCustomPermissions") + .HasColumnType("INTEGER"); + + b.Property("UseDirectory") + .HasColumnType("INTEGER"); + + b.Property("UseDisableSmAdsForUsers") + .HasColumnType("INTEGER"); + + b.Property("UseEvents") + .HasColumnType("INTEGER"); + + b.Property("UseGroups") + .HasColumnType("INTEGER"); + + b.Property("UseKeyConnector") + .HasColumnType("INTEGER"); + + b.Property("UseOrganizationDomains") + .HasColumnType("INTEGER"); + + b.Property("UsePasswordManager") + .HasColumnType("INTEGER"); + + b.Property("UsePhishingBlocker") + .HasColumnType("INTEGER"); + + b.Property("UsePolicies") + .HasColumnType("INTEGER"); + + b.Property("UseResetPassword") + .HasColumnType("INTEGER"); + + b.Property("UseRiskInsights") + .HasColumnType("INTEGER"); + + b.Property("UseScim") + .HasColumnType("INTEGER"); + + b.Property("UseSecretsManager") + .HasColumnType("INTEGER"); + + b.Property("UseSso") + .HasColumnType("INTEGER"); + + b.Property("UseTotp") + .HasColumnType("INTEGER"); + + b.Property("UsersGetPremium") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Enabled") + .HasAnnotation("Npgsql:IndexInclude", new[] { "UseTotp", "UsersGetPremium" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Policy", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("BillingEmail") + .HasColumnType("TEXT"); + + b.Property("BillingPhone") + .HasColumnType("TEXT"); + + b.Property("BusinessAddress1") + .HasColumnType("TEXT"); + + b.Property("BusinessAddress2") + .HasColumnType("TEXT"); + + b.Property("BusinessAddress3") + .HasColumnType("TEXT"); + + b.Property("BusinessCountry") + .HasColumnType("TEXT"); + + b.Property("BusinessName") + .HasColumnType("TEXT"); + + b.Property("BusinessTaxNumber") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DiscountId") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayCustomerId") + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UseEvents") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Provider", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Settings") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderOrganization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId"); + + b.ToTable("ProviderUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessCode") + .HasMaxLength(25) + .HasColumnType("TEXT"); + + b.Property("Approved") + .HasColumnType("INTEGER"); + + b.Property("AuthenticationDate") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("MasterPasswordHash") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasColumnType("TEXT"); + + b.Property("RequestCountryName") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("RequestDeviceIdentifier") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RequestDeviceType") + .HasColumnType("INTEGER"); + + b.Property("RequestIpAddress") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ResponseDate") + .HasColumnType("TEXT"); + + b.Property("ResponseDeviceId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ResponseDeviceId"); + + b.HasIndex("UserId"); + + b.ToTable("AuthRequest", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("GranteeId") + .HasColumnType("TEXT"); + + b.Property("GrantorId") + .HasColumnType("TEXT"); + + b.Property("KeyEncrypted") + .HasColumnType("TEXT"); + + b.Property("LastNotificationDate") + .HasColumnType("TEXT"); + + b.Property("RecoveryInitiatedDate") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("WaitTimeDays") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GranteeId"); + + b.HasIndex("GrantorId"); + + b.ToTable("EmergencyAccess", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("ConsumedDate") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasName("PK_Grant") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpirationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Grant", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SsoConfig", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId"); + + b.HasIndex("OrganizationId", "ExternalId") + .IsUnique() + .HasAnnotation("Npgsql:IndexInclude", new[] { "UserId" }) + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SsoUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AaGuid") + .HasColumnType("TEXT"); + + b.Property("Counter") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("CredentialId") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EncryptedPrivateKey") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("EncryptedPublicKey") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("EncryptedUserKey") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SupportsPrf") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("WebAuthnCredential", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ClientOrganizationMigrationRecord", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("GatewayCustomerId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("INTEGER"); + + b.Property("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PlanType") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Seats") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId", "OrganizationId") + .IsUnique(); + + b.ToTable("ClientOrganizationMigrationRecord", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("InstallationId") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("InstallationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInstallation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AssignedSeats") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("TEXT"); + + b.Property("ClientName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("InvoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("InvoiceNumber") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PlanName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Total") + .HasColumnType("TEXT"); + + b.Property("UsedSeats") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderInvoiceItem", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllocatedSeats") + .HasColumnType("INTEGER"); + + b.Property("PlanType") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("PurchasedSeats") + .HasColumnType("INTEGER"); + + b.Property("SeatMinimum") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("Id", "PlanType") + .IsUnique(); + + b.ToTable("ProviderPlan", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationApplication", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Applications") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegration", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Configuration") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationIntegration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegrationConfiguration", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Configuration") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("EventType") + .HasColumnType("INTEGER"); + + b.Property("Filters") + .HasColumnType("TEXT"); + + b.Property("OrganizationIntegrationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Template") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationIntegrationId"); + + b.ToTable("OrganizationIntegrationConfiguration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationReport", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ApplicationAtRiskCount") + .HasColumnType("INTEGER"); + + b.Property("ApplicationCount") + .HasColumnType("INTEGER"); + + b.Property("ApplicationData") + .HasColumnType("TEXT"); + + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("CriticalApplicationAtRiskCount") + .HasColumnType("INTEGER"); + + b.Property("CriticalApplicationCount") + .HasColumnType("INTEGER"); + + b.Property("CriticalMemberAtRiskCount") + .HasColumnType("INTEGER"); + + b.Property("CriticalMemberCount") + .HasColumnType("INTEGER"); + + b.Property("CriticalPasswordAtRiskCount") + .HasColumnType("INTEGER"); + + b.Property("CriticalPasswordCount") + .HasColumnType("INTEGER"); + + b.Property("MemberAtRiskCount") + .HasColumnType("INTEGER"); + + b.Property("MemberCount") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PasswordAtRiskCount") + .HasColumnType("INTEGER"); + + b.Property("PasswordCount") + .HasColumnType("INTEGER"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SummaryData") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationReport", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Uri") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b => + { + b.Property("Id") + .HasMaxLength(449) + .HasColumnType("TEXT"); + + b.Property("AbsoluteExpiration") + .HasColumnType("TEXT"); + + b.Property("ExpiresAtTime") + .HasColumnType("TEXT"); + + b.Property("SlidingExpirationInSeconds") + .HasColumnType("INTEGER"); + + b.Property("Value") + .IsRequired() + .HasColumnType("BLOB"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpiresAtTime") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DefaultUserCollectionEmail") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("HidePasswords") + .HasColumnType("INTEGER"); + + b.Property("Manage") + .HasColumnType("INTEGER"); + + b.Property("ReadOnly") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("OrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("HidePasswords") + .HasColumnType("INTEGER"); + + b.Property("Manage") + .HasColumnType("INTEGER"); + + b.Property("ReadOnly") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Active") + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("EncryptedPrivateKey") + .HasColumnType("TEXT"); + + b.Property("EncryptedPublicKey") + .HasColumnType("TEXT"); + + b.Property("EncryptedUserKey") + .HasColumnType("TEXT"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "Identifier") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Device", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Event", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActingUserId") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("DeviceType") + .HasColumnType("INTEGER"); + + b.Property("DomainName") + .HasColumnType("TEXT"); + + b.Property("GrantedServiceAccountId") + .HasColumnType("TEXT"); + + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("InstallationId") + .HasColumnType("TEXT"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("OrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("PolicyId") + .HasColumnType("TEXT"); + + b.Property("ProjectId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("ProviderOrganizationId") + .HasColumnType("TEXT"); + + b.Property("ProviderUserId") + .HasColumnType("TEXT"); + + b.Property("SecretId") + .HasColumnType("TEXT"); + + b.Property("ServiceAccountId") + .HasColumnType("TEXT"); + + b.Property("SystemUser") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasDatabaseName("IX_Event_DateOrganizationIdUserId") + .HasAnnotation("SqlServer:Clustered", false) + .HasAnnotation("SqlServer:Include", new[] { "ServiceAccountId", "GrantedServiceAccountId" }); + + b.ToTable("Event", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("OrganizationUserId") + .HasColumnType("TEXT"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("GroupUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Config") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("JobRunCount") + .HasColumnType("INTEGER"); + + b.Property("LastCheckedDate") + .HasColumnType("TEXT"); + + b.Property("NextRunDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("VerifiedDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("IsAdminInitiated") + .HasColumnType("INTEGER"); + + b.Property("LastSyncDate") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasColumnType("TEXT"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PlanSponsorshipType") + .HasColumnType("INTEGER"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("TEXT"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("TEXT"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("ToDelete") + .HasColumnType("INTEGER"); + + b.Property("ValidUntil") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessSecretsManager") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("TEXT"); + + b.Property("ResetPasswordKey") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.PlayItem", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PlayId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("PlayId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PlayItem", null, t => + { + t.HasCheckConstraint("CK_PlayItem_UserOrOrganization", "(\"UserId\" IS NOT NULL AND \"OrganizationId\" IS NULL) OR (\"UserId\" IS NULL AND \"OrganizationId\" IS NOT NULL)"); + }); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessCount") + .HasColumnType("INTEGER"); + + b.Property("AuthType") + .HasColumnType("INTEGER"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("DeletionDate") + .HasColumnType("TEXT"); + + b.Property("Disabled") + .HasColumnType("INTEGER"); + + b.Property("Emails") + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("HideEmail") + .HasColumnType("INTEGER"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("MaxAccessCount") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PostalCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("Rate") + .HasColumnType("TEXT"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("TaxRate", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PaymentMethodType") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Refunded") + .HasColumnType("INTEGER"); + + b.Property("RefundedAmount") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountRevisionDate") + .HasColumnType("TEXT"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("AvatarColor") + .HasMaxLength(7) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailVerified") + .HasColumnType("INTEGER"); + + b.Property("EquivalentDomains") + .HasColumnType("TEXT"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("TEXT"); + + b.Property("FailedLoginCount") + .HasColumnType("INTEGER"); + + b.Property("ForcePasswordReset") + .HasColumnType("INTEGER"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Kdf") + .HasColumnType("INTEGER"); + + b.Property("KdfIterations") + .HasColumnType("INTEGER"); + + b.Property("KdfMemory") + .HasColumnType("INTEGER"); + + b.Property("KdfParallelism") + .HasColumnType("INTEGER"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("LastEmailChangeDate") + .HasColumnType("TEXT"); + + b.Property("LastFailedLoginDate") + .HasColumnType("TEXT"); + + b.Property("LastKdfChangeDate") + .HasColumnType("TEXT"); + + b.Property("LastKeyRotationDate") + .HasColumnType("TEXT"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("TEXT"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Premium") + .HasColumnType("INTEGER"); + + b.Property("PremiumExpirationDate") + .HasColumnType("TEXT"); + + b.Property("PrivateKey") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasColumnType("TEXT"); + + b.Property("ReferenceData") + .HasColumnType("TEXT"); + + b.Property("RenewalReminderDate") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("SecurityState") + .HasColumnType("TEXT"); + + b.Property("SecurityVersion") + .HasColumnType("INTEGER"); + + b.Property("SignedPublicKey") + .HasColumnType("TEXT"); + + b.Property("Storage") + .HasColumnType("INTEGER"); + + b.Property("TwoFactorProviders") + .HasColumnType("TEXT"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UsesKeyConnector") + .HasColumnType("INTEGER"); + + b.Property("VerifyDevices") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.UserSignatureKeyPair", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SignatureAlgorithm") + .HasColumnType("INTEGER"); + + b.Property("SigningKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("VerifyingKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("UserSignatureKeyPair", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Body") + .HasMaxLength(3000) + .HasColumnType("TEXT"); + + b.Property("ClientType") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Global") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Priority") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("TaskId") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("TaskId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ClientType", "Global", "UserId", "OrganizationId", "Priority", "CreationDate") + .IsDescending(false, false, false, false, true, true) + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("NotificationId") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("ReadDate") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "NotificationId") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("NotificationId"); + + b.ToTable("NotificationStatus", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Platform.Installation", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("TEXT"); + + b.Property("LastActivityDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("TEXT"); + + b.Property("Read") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Write") + .HasColumnType("INTEGER"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("ExpireAt") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("ServiceAccountId") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("Note") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.SecretVersion", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("EditorOrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("EditorServiceAccountId") + .HasColumnType("TEXT"); + + b.Property("SecretId") + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("VersionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("EditorOrganizationUserId") + .HasDatabaseName("IX_SecretVersion_EditorOrganizationUserId"); + + b.HasIndex("EditorServiceAccountId") + .HasDatabaseName("IX_SecretVersion_EditorServiceAccountId"); + + b.HasIndex("SecretId") + .HasDatabaseName("IX_SecretVersion_SecretId"); + + b.ToTable("SecretVersion"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Archives") + .HasColumnType("TEXT"); + + b.Property("Attachments") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Favorites") + .HasColumnType("TEXT"); + + b.Property("Folders") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Reprompt") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("TEXT"); + + b.Property("SecretsId") + .HasColumnType("TEXT"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Device", "ResponseDevice") + .WithMany() + .HasForeignKey("ResponseDeviceId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("ResponseDevice"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Platform.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Installation"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegrationConfiguration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegration", "OrganizationIntegration") + .WithMany() + .HasForeignKey("OrganizationIntegrationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationIntegration"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationReport", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.PasswordHealthReportApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Collections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("GroupUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Domains") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.PlayItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.UserSignatureKeyPair", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", "Task") + .WithMany() + .HasForeignKey("TaskId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Task"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", "Notification") + .WithMany() + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notification"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ApiKeys") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.SecretVersion", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "EditorOrganizationUser") + .WithMany() + .HasForeignKey("EditorOrganizationUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "EditorServiceAccount") + .WithMany() + .HasForeignKey("EditorServiceAccountId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "Secret") + .WithMany("SecretVersions") + .HasForeignKey("SecretId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EditorOrganizationUser"); + + b.Navigation("EditorServiceAccount"); + + b.Navigation("Secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Ciphers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Ciphers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Folders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany() + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", null) + .WithMany() + .HasForeignKey("ProjectsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", null) + .WithMany() + .HasForeignKey("SecretsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedProject"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedSecret"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ProjectAccessPolicies") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedProject"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany() + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedProject"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("Ciphers"); + + b.Navigation("Collections"); + + b.Navigation("Connections"); + + b.Navigation("Domains"); + + b.Navigation("Groups"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Policies"); + + b.Navigation("SsoConfigs"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Navigation("CollectionUsers"); + + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Navigation("Ciphers"); + + b.Navigation("Folders"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("SecretVersions"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ProjectAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Navigation("CollectionCiphers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/util/SqliteMigrations/Migrations/20260108193841_CreatePlayItem.cs b/util/SqliteMigrations/Migrations/20260108193841_CreatePlayItem.cs new file mode 100644 index 0000000000..ae62e38f23 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20260108193841_CreatePlayItem.cs @@ -0,0 +1,63 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations; + +/// +public partial class CreatePlayItem : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "PlayItem", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + PlayId = table.Column(type: "TEXT", maxLength: 256, nullable: false), + UserId = table.Column(type: "TEXT", nullable: true), + OrganizationId = table.Column(type: "TEXT", nullable: true), + CreationDate = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_PlayItem", x => x.Id); + table.CheckConstraint("CK_PlayItem_UserOrOrganization", "(\"UserId\" IS NOT NULL AND \"OrganizationId\" IS NULL) OR (\"UserId\" IS NULL AND \"OrganizationId\" IS NOT NULL)"); + table.ForeignKey( + name: "FK_PlayItem_Organization_OrganizationId", + column: x => x.OrganizationId, + principalTable: "Organization", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_PlayItem_User_UserId", + column: x => x.UserId, + principalTable: "User", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_PlayItem_OrganizationId", + table: "PlayItem", + column: "OrganizationId"); + + migrationBuilder.CreateIndex( + name: "IX_PlayItem_PlayId", + table: "PlayItem", + column: "PlayId"); + + migrationBuilder.CreateIndex( + name: "IX_PlayItem_UserId", + table: "PlayItem", + column: "UserId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "PlayItem"); + } +} diff --git a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs index a30b959ce9..4605ae8939 100644 --- a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -277,71 +277,6 @@ namespace Bit.SqliteMigrations.Migrations b.ToTable("Organization", (string)null); }); - modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Configuration") - .HasColumnType("TEXT"); - - b.Property("CreationDate") - .HasColumnType("TEXT"); - - b.Property("OrganizationId") - .HasColumnType("TEXT"); - - b.Property("RevisionDate") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("OrganizationId") - .HasAnnotation("SqlServer:Clustered", false); - - b.HasIndex("OrganizationId", "Type") - .IsUnique() - .HasAnnotation("SqlServer:Clustered", false); - - b.ToTable("OrganizationIntegration", (string)null); - }); - - modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Configuration") - .HasColumnType("TEXT"); - - b.Property("CreationDate") - .HasColumnType("TEXT"); - - b.Property("EventType") - .HasColumnType("INTEGER"); - - b.Property("Filters") - .HasColumnType("TEXT"); - - b.Property("OrganizationIntegrationId") - .HasColumnType("TEXT"); - - b.Property("RevisionDate") - .HasColumnType("TEXT"); - - b.Property("Template") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("OrganizationIntegrationId"); - - b.ToTable("OrganizationIntegrationConfiguration", (string)null); - }); - modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => { b.Property("Id") @@ -621,7 +556,7 @@ namespace Bit.SqliteMigrations.Migrations b.Property("Type") .HasColumnType("INTEGER"); - b.Property("WaitTimeDays") + b.Property("WaitTimeDays") .HasColumnType("INTEGER"); b.HasKey("Id"); @@ -1004,6 +939,71 @@ namespace Bit.SqliteMigrations.Migrations b.ToTable("OrganizationApplication", (string)null); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegration", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Configuration") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationIntegration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegrationConfiguration", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Configuration") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("EventType") + .HasColumnType("INTEGER"); + + b.Property("Filters") + .HasColumnType("TEXT"); + + b.Property("OrganizationIntegrationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Template") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationIntegrationId"); + + b.ToTable("OrganizationIntegrationConfiguration", (string)null); + }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationReport", b => { b.Property("Id") @@ -1616,6 +1616,42 @@ namespace Bit.SqliteMigrations.Migrations b.ToTable("OrganizationUser", (string)null); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.PlayItem", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PlayId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("PlayId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PlayItem", null, t => + { + t.HasCheckConstraint("CK_PlayItem_UserOrOrganization", "(\"UserId\" IS NOT NULL AND \"OrganizationId\" IS NULL) OR (\"UserId\" IS NULL AND \"OrganizationId\" IS NOT NULL)"); + }); + }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => { b.Property("Id") @@ -2596,28 +2632,6 @@ namespace Bit.SqliteMigrations.Migrations b.HasDiscriminator().HasValue("user_service_account"); }); - modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b => - { - b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") - .WithMany() - .HasForeignKey("OrganizationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Organization"); - }); - - modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b => - { - b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", "OrganizationIntegration") - .WithMany() - .HasForeignKey("OrganizationIntegrationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("OrganizationIntegration"); - }); - modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => { b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") @@ -2796,6 +2810,28 @@ namespace Bit.SqliteMigrations.Migrations b.Navigation("Organization"); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegrationConfiguration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegration", "OrganizationIntegration") + .WithMany() + .HasForeignKey("OrganizationIntegrationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationIntegration"); + }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationReport", b => { b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") @@ -2992,6 +3028,23 @@ namespace Bit.SqliteMigrations.Migrations b.Navigation("User"); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.PlayItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => { b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") diff --git a/util/SqliteMigrations/SqliteMigrations.csproj b/util/SqliteMigrations/SqliteMigrations.csproj index dce863036f..26def0dad2 100644 --- a/util/SqliteMigrations/SqliteMigrations.csproj +++ b/util/SqliteMigrations/SqliteMigrations.csproj @@ -3,6 +3,8 @@ enable enable + + $(WarningsNotAsErrors);CA1305