From b07df10335132831722b138fb4970dc624df1833 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Wed, 6 Nov 2024 18:12:59 -0500 Subject: [PATCH 01/29] Add back provider keys on TwoFactorProviders response (#4991) --- .../RequestValidators/TwoFactorAuthenticationValidator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Identity/IdentityServer/RequestValidators/TwoFactorAuthenticationValidator.cs b/src/Identity/IdentityServer/RequestValidators/TwoFactorAuthenticationValidator.cs index 323d09c0e..4cabcf4fa 100644 --- a/src/Identity/IdentityServer/RequestValidators/TwoFactorAuthenticationValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/TwoFactorAuthenticationValidator.cs @@ -121,8 +121,8 @@ public class TwoFactorAuthenticationValidator( var twoFactorResultDict = new Dictionary { - { "TwoFactorProviders", null }, - { "TwoFactorProviders2", providers }, // backwards compatibility + { "TwoFactorProviders", providers.Keys }, // backwards compatibility + { "TwoFactorProviders2", providers }, }; // If we have email as a 2FA provider, we might need an SsoEmail2fa Session Token From f7957f7053c1e60d724dca825d509107ae7fbd7d Mon Sep 17 00:00:00 2001 From: Matt Bishop Date: Thu, 7 Nov 2024 09:56:57 -0500 Subject: [PATCH 02/29] Check run permissions for secrets usage (#4992) --- .github/workflows/build.yml | 41 ++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 26a347781..cc305eb91 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,18 +7,25 @@ on: - "main" - "rc" - "hotfix-rc" - pull_request: + pull_request_target: + types: [opened, synchronize] env: _AZ_REGISTRY: "bitwardenprod.azurecr.io" jobs: + check-run: + name: Check PR run + uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main + lint: name: Lint runs-on: ubuntu-22.04 steps: - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Set up .NET uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0 @@ -29,8 +36,7 @@ jobs: build-artifacts: name: Build artifacts runs-on: ubuntu-22.04 - needs: - - lint + needs: lint strategy: fail-fast: false matrix: @@ -68,6 +74,8 @@ jobs: steps: - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Set up .NET uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0 @@ -115,24 +123,6 @@ jobs: path: ${{ matrix.base_path }}/${{ matrix.project_name }}/${{ matrix.project_name }}.zip if-no-files-found: error - check-akv-secrets: - name: Check for AKV secrets - runs-on: ubuntu-22.04 - outputs: - available: ${{ steps.check-akv-secrets.outputs.available }} - permissions: - contents: read - - steps: - - name: Check - id: check-akv-secrets - run: | - if [ "${{ secrets.AZURE_PROD_KV_CREDENTIALS }}" != '' ]; then - echo "available=true" >> $GITHUB_OUTPUT; - else - echo "available=false" >> $GITHUB_OUTPUT; - fi - build-docker: name: Build Docker images runs-on: ubuntu-22.04 @@ -140,8 +130,7 @@ jobs: security-events: write needs: - build-artifacts - - check-akv-secrets - if: ${{ needs.check-akv-secrets.outputs.available == 'true' }} + - check-run strategy: fail-fast: false matrix: @@ -194,6 +183,8 @@ jobs: steps: - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Check branch to publish env: @@ -313,6 +304,8 @@ jobs: steps: - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Set up .NET uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0 @@ -488,6 +481,8 @@ jobs: steps: - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Set up .NET uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0 From 72736db4b6d43407f8fd47b6a12ca801b649d33f Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Thu, 7 Nov 2024 10:21:48 -0600 Subject: [PATCH 03/29] [PM-13839][PM-13840] Admin Console Collections (#4922) * add collectionIds to the response of `{id}/admin` - They're now needed in the admin console when add/editing a cipher. - Prior to this there was no way to edit collection when editing a cipher. Assigning collections was a separate workflow * return cipher from collections endpoint --- src/Api/Vault/Controllers/CiphersController.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Api/Vault/Controllers/CiphersController.cs b/src/Api/Vault/Controllers/CiphersController.cs index 09ade4d0d..59984683e 100644 --- a/src/Api/Vault/Controllers/CiphersController.cs +++ b/src/Api/Vault/Controllers/CiphersController.cs @@ -99,7 +99,10 @@ public class CiphersController : Controller throw new NotFoundException(); } - return new CipherMiniResponseModel(cipher, _globalSettings, cipher.OrganizationUseTotp); + var collectionCiphers = await _collectionCipherRepository.GetManyByOrganizationIdAsync(cipher.OrganizationId.Value); + var collectionCiphersGroupDict = collectionCiphers.GroupBy(c => c.CipherId).ToDictionary(s => s.Key); + + return new CipherMiniDetailsResponseModel(cipher, _globalSettings, collectionCiphersGroupDict, cipher.OrganizationUseTotp); } [HttpGet("{id}/full-details")] @@ -600,10 +603,10 @@ public class CiphersController : Controller [HttpPut("{id}/collections-admin")] [HttpPost("{id}/collections-admin")] - public async Task PutCollectionsAdmin(string id, [FromBody] CipherCollectionsRequestModel model) + public async Task PutCollectionsAdmin(string id, [FromBody] CipherCollectionsRequestModel model) { var userId = _userService.GetProperUserId(User).Value; - var cipher = await _cipherRepository.GetByIdAsync(new Guid(id)); + var cipher = await _cipherRepository.GetOrganizationDetailsByIdAsync(new Guid(id)); if (cipher == null || !cipher.OrganizationId.HasValue || !await CanEditCipherAsAdminAsync(cipher.OrganizationId.Value, new[] { cipher.Id })) @@ -621,6 +624,11 @@ public class CiphersController : Controller } await _cipherService.SaveCollectionsAsync(cipher, collectionIds, userId, true); + + var collectionCiphers = await _collectionCipherRepository.GetManyByOrganizationIdAsync(cipher.OrganizationId.Value); + var collectionCiphersGroupDict = collectionCiphers.GroupBy(c => c.CipherId).ToDictionary(s => s.Key); + + return new CipherMiniDetailsResponseModel(cipher, _globalSettings, collectionCiphersGroupDict, cipher.OrganizationUseTotp); } [HttpPost("bulk-collections")] From 82cd1a8b1a38f24174a2be1dbbd93c2f958a161d Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Thu, 7 Nov 2024 11:30:26 -0500 Subject: [PATCH 04/29] add feature flag (#4987) --- src/Core/Constants.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 0bc6393d3..e152966e5 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -151,6 +151,7 @@ public static class FeatureFlagKeys public const string GeneratorToolsModernization = "generator-tools-modernization"; public const string NewDeviceVerification = "new-device-verification"; public const string RiskInsightsCriticalApplication = "pm-14466-risk-insights-critical-application"; + public const string IntegrationPage = "pm-14505-admin-console-integration-page"; public static List GetAllKeys() { From 4adcecb80a980f9f9690c5f19e22072e61f5038d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 7 Nov 2024 11:51:39 -0500 Subject: [PATCH 05/29] [deps]: Update Microsoft.NET.Test.Sdk to 17.11.1 (#4830) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .../Infrastructure.Dapper.Test.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Infrastructure.Dapper.Test/Infrastructure.Dapper.Test.csproj b/test/Infrastructure.Dapper.Test/Infrastructure.Dapper.Test.csproj index db5913d72..dfc8951cc 100644 --- a/test/Infrastructure.Dapper.Test/Infrastructure.Dapper.Test.csproj +++ b/test/Infrastructure.Dapper.Test/Infrastructure.Dapper.Test.csproj @@ -10,7 +10,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive From 15bc5060c6893987d14cbbda7c238c159036b191 Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Thu, 7 Nov 2024 14:10:00 -0500 Subject: [PATCH 06/29] [PM-11409] prevent managed user from leaving managing organization (#4995) * prevent managed user from leaving managing organization * fix org check to be specific to single org * simplify logic --- .../Controllers/OrganizationsController.cs | 6 ++++ .../OrganizationsControllerTests.cs | 36 +++++++++++++++++-- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/Api/AdminConsole/Controllers/OrganizationsController.cs b/src/Api/AdminConsole/Controllers/OrganizationsController.cs index 0b3811618..e134adc04 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationsController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationsController.cs @@ -252,6 +252,12 @@ public class OrganizationsController : Controller throw new BadRequestException("Your organization's Single Sign-On settings prevent you from leaving."); } + if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) + && (await _userService.GetOrganizationsManagingUserAsync(user.Id)).Any(x => x.Id == id)) + { + throw new BadRequestException("Managed user account cannot leave managing organization. Contact your organization administrator for additional details."); + } + await _removeOrganizationUserCommand.RemoveUserAsync(id, user.Id); } diff --git a/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs index 25227fec7..13826888d 100644 --- a/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs @@ -51,7 +51,6 @@ public class OrganizationsControllerTests : IDisposable private readonly IProviderBillingService _providerBillingService; private readonly IDataProtectorTokenFactory _orgDeleteTokenDataFactory; private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; - private readonly OrganizationsController _sut; public OrganizationsControllerTests() @@ -123,7 +122,8 @@ public class OrganizationsControllerTests : IDisposable _currentContext.OrganizationUser(orgId).Returns(true); _ssoConfigRepository.GetByOrganizationIdAsync(orgId).Returns(ssoConfig); _userService.GetUserByPrincipalAsync(Arg.Any()).Returns(user); - + _featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true); + _userService.GetOrganizationsManagingUserAsync(user.Id).Returns(new List { null }); var exception = await Assert.ThrowsAsync(() => _sut.Leave(orgId)); Assert.Contains("Your organization's Single Sign-On settings prevent you from leaving.", @@ -132,6 +132,36 @@ public class OrganizationsControllerTests : IDisposable await _removeOrganizationUserCommand.DidNotReceiveWithAnyArgs().RemoveUserAsync(default, default); } + [Theory, AutoData] + public async Task OrganizationsController_UserCannotLeaveOrganizationThatManagesUser( + Guid orgId, User user) + { + var ssoConfig = new SsoConfig + { + Id = default, + Data = new SsoConfigurationData + { + MemberDecryptionType = MemberDecryptionType.KeyConnector + }.Serialize(), + Enabled = true, + OrganizationId = orgId, + }; + var foundOrg = new Organization(); + foundOrg.Id = orgId; + + _currentContext.OrganizationUser(orgId).Returns(true); + _ssoConfigRepository.GetByOrganizationIdAsync(orgId).Returns(ssoConfig); + _userService.GetUserByPrincipalAsync(Arg.Any()).Returns(user); + _featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true); + _userService.GetOrganizationsManagingUserAsync(user.Id).Returns(new List { { foundOrg } }); + var exception = await Assert.ThrowsAsync(() => _sut.Leave(orgId)); + + Assert.Contains("Managed user account cannot leave managing organization. Contact your organization administrator for additional details.", + exception.Message); + + await _removeOrganizationUserCommand.DidNotReceiveWithAnyArgs().RemoveUserAsync(default, default); + } + [Theory] [InlineAutoData(true, false)] [InlineAutoData(false, true)] @@ -157,6 +187,8 @@ public class OrganizationsControllerTests : IDisposable _currentContext.OrganizationUser(orgId).Returns(true); _ssoConfigRepository.GetByOrganizationIdAsync(orgId).Returns(ssoConfig); _userService.GetUserByPrincipalAsync(Arg.Any()).Returns(user); + _featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true); + _userService.GetOrganizationsManagingUserAsync(user.Id).Returns(new List()); await _sut.Leave(orgId); From ebd78ff30df67ec13ffc3405044b94f7cc4976de Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Thu, 7 Nov 2024 14:14:42 -0500 Subject: [PATCH 07/29] [PM-11408] Remove cs delete permission (#4998) * remove user delete permission from CS role --- src/Admin/Utilities/RolePermissionMapping.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Admin/Utilities/RolePermissionMapping.cs b/src/Admin/Utilities/RolePermissionMapping.cs index ec357c7e9..e260c264f 100644 --- a/src/Admin/Utilities/RolePermissionMapping.cs +++ b/src/Admin/Utilities/RolePermissionMapping.cs @@ -110,7 +110,6 @@ public static class RolePermissionMapping Permission.User_Licensing_View, Permission.User_Billing_View, Permission.User_Billing_LaunchGateway, - Permission.User_Delete, Permission.Org_List_View, Permission.Org_OrgInformation_View, Permission.Org_GeneralDetails_View, From fda7c4912aebdbef84a919827a88fef99fc599ae Mon Sep 17 00:00:00 2001 From: Jason Ng Date: Thu, 7 Nov 2024 14:30:29 -0500 Subject: [PATCH 08/29] [PM-8682] added flags for new device verification notice (#4999) --- src/Core/Constants.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index e152966e5..8e759e143 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -152,6 +152,8 @@ public static class FeatureFlagKeys public const string NewDeviceVerification = "new-device-verification"; public const string RiskInsightsCriticalApplication = "pm-14466-risk-insights-critical-application"; public const string IntegrationPage = "pm-14505-admin-console-integration-page"; + public const string NewDeviceVerificationTemporaryDismiss = "new-device-temporary-dismiss"; + public const string NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss"; public static List GetAllKeys() { From d6e624d6394d6fa8fdc71587584bb2d8eb372f44 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 7 Nov 2024 20:39:36 +0100 Subject: [PATCH 09/29] [deps] Tools: Update aws-sdk-net monorepo (#4993) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- src/Core/Core.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 6913a1e89..338b908da 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -21,8 +21,8 @@ - - + + From 21b7c3b73af5c0f0e5a781344f34c6af7f341aff Mon Sep 17 00:00:00 2001 From: Matt Bishop Date: Thu, 7 Nov 2024 16:13:57 -0500 Subject: [PATCH 10/29] Support client version prerelease flag in context and LD targeting (#4994) * Support client version prerelease flag in context and LD targeting * Use integer instead of Boolean --- src/Core/Context/CurrentContext.cs | 6 ++++++ src/Core/Context/ICurrentContext.cs | 3 ++- .../Services/Implementations/LaunchDarklyFeatureService.cs | 2 ++ test/Core.Test/Services/LaunchDarklyFeatureServiceTests.cs | 1 + 4 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Core/Context/CurrentContext.cs b/src/Core/Context/CurrentContext.cs index cbbcb9f20..e64615753 100644 --- a/src/Core/Context/CurrentContext.cs +++ b/src/Core/Context/CurrentContext.cs @@ -39,6 +39,7 @@ public class CurrentContext : ICurrentContext public virtual int? BotScore { get; set; } public virtual string ClientId { get; set; } public virtual Version ClientVersion { get; set; } + public virtual bool ClientVersionIsPrerelease { get; set; } public virtual IdentityClientType IdentityClientType { get; set; } public virtual Guid? ServiceAccountOrganizationId { get; set; } @@ -97,6 +98,11 @@ public class CurrentContext : ICurrentContext { ClientVersion = cVersion; } + + if (httpContext.Request.Headers.TryGetValue("Is-Prerelease", out var clientVersionIsPrerelease)) + { + ClientVersionIsPrerelease = clientVersionIsPrerelease == "1"; + } } public async virtual Task BuildAsync(ClaimsPrincipal user, GlobalSettings globalSettings) diff --git a/src/Core/Context/ICurrentContext.cs b/src/Core/Context/ICurrentContext.cs index e3f737698..3d3a5960b 100644 --- a/src/Core/Context/ICurrentContext.cs +++ b/src/Core/Context/ICurrentContext.cs @@ -29,12 +29,13 @@ public interface ICurrentContext int? BotScore { get; set; } string ClientId { get; set; } Version ClientVersion { get; set; } + bool ClientVersionIsPrerelease { get; set; } + Task BuildAsync(HttpContext httpContext, GlobalSettings globalSettings); Task BuildAsync(ClaimsPrincipal user, GlobalSettings globalSettings); Task SetContextAsync(ClaimsPrincipal user); - Task OrganizationUser(Guid orgId); Task OrganizationAdmin(Guid orgId); Task OrganizationOwner(Guid orgId); diff --git a/src/Core/Services/Implementations/LaunchDarklyFeatureService.cs b/src/Core/Services/Implementations/LaunchDarklyFeatureService.cs index b65aa7525..48d8fa122 100644 --- a/src/Core/Services/Implementations/LaunchDarklyFeatureService.cs +++ b/src/Core/Services/Implementations/LaunchDarklyFeatureService.cs @@ -20,6 +20,7 @@ public class LaunchDarklyFeatureService : IFeatureService private const string _contextKindServiceAccount = "service-account"; private const string _contextAttributeClientVersion = "client-version"; + private const string _contextAttributeClientVersionIsPrerelease = "client-version-is-prerelease"; private const string _contextAttributeDeviceType = "device-type"; private const string _contextAttributeClientType = "client-type"; private const string _contextAttributeOrganizations = "organizations"; @@ -145,6 +146,7 @@ public class LaunchDarklyFeatureService : IFeatureService if (_currentContext.ClientVersion != null) { builder.Set(_contextAttributeClientVersion, _currentContext.ClientVersion.ToString()); + builder.Set(_contextAttributeClientVersionIsPrerelease, _currentContext.ClientVersionIsPrerelease); } if (_currentContext.DeviceType.HasValue) diff --git a/test/Core.Test/Services/LaunchDarklyFeatureServiceTests.cs b/test/Core.Test/Services/LaunchDarklyFeatureServiceTests.cs index 08de8e320..35b5e4ea7 100644 --- a/test/Core.Test/Services/LaunchDarklyFeatureServiceTests.cs +++ b/test/Core.Test/Services/LaunchDarklyFeatureServiceTests.cs @@ -24,6 +24,7 @@ public class LaunchDarklyFeatureServiceTests var currentContext = Substitute.For(); currentContext.UserId.Returns(Guid.NewGuid()); currentContext.ClientVersion.Returns(new Version(AssemblyHelpers.GetVersion())); + currentContext.ClientVersionIsPrerelease.Returns(true); currentContext.DeviceType.Returns(Enums.DeviceType.ChromeBrowser); var client = Substitute.For(); From fcb706b9c54a4d0c45f03ae236334e6f883a98b9 Mon Sep 17 00:00:00 2001 From: Matt Bishop Date: Thu, 7 Nov 2024 17:11:01 -0500 Subject: [PATCH 11/29] Catch PR targets for certain build operations (#5003) * Catch PR targets for certain build operations * Support EE --- .github/workflows/build.yml | 37 ++++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cc305eb91..0a3489ead 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -319,9 +319,9 @@ jobs: run: az acr login -n $_AZ_REGISTRY --only-show-errors - name: Make Docker stubs - if: github.ref == 'refs/heads/main' || - github.ref == 'refs/heads/rc' || - github.ref == 'refs/heads/hotfix-rc' + if: | + github.event_name != 'pull_request_target' + && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc') run: | # Set proper setup image based on branch case "$GITHUB_REF" in @@ -361,13 +361,17 @@ jobs: cd docker-stub/EU; zip -r ../../docker-stub-EU.zip *; cd ../.. - name: Make Docker stub checksums - if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc' + if: | + github.event_name != 'pull_request_target' + && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc') run: | sha256sum docker-stub-US.zip > docker-stub-US-sha256.txt sha256sum docker-stub-EU.zip > docker-stub-EU-sha256.txt - name: Upload Docker stub US artifact - if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc' + if: | + github.event_name != 'pull_request_target' + && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc') uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: docker-stub-US.zip @@ -375,7 +379,9 @@ jobs: if-no-files-found: error - name: Upload Docker stub EU artifact - if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc' + if: | + github.event_name != 'pull_request_target' + && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc') uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: docker-stub-EU.zip @@ -383,7 +389,9 @@ jobs: if-no-files-found: error - name: Upload Docker stub US checksum artifact - if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc' + if: | + github.event_name != 'pull_request_target' + && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc') uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: docker-stub-US-sha256.txt @@ -391,7 +399,9 @@ jobs: if-no-files-found: error - name: Upload Docker stub EU checksum artifact - if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc' + if: | + github.event_name != 'pull_request_target' + && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc') uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: docker-stub-EU-sha256.txt @@ -549,7 +559,7 @@ jobs: trigger-k8s-deploy: name: Trigger k8s deploy - if: github.ref == 'refs/heads/main' + if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main' runs-on: ubuntu-22.04 needs: build-docker steps: @@ -583,7 +593,9 @@ jobs: trigger-ee-updates: name: Trigger Ephemeral Environment updates - if: github.ref != 'refs/heads/main' && contains(github.event.pull_request.labels.*.name, 'ephemeral-environment') + if: | + github.event_name == 'pull_request_target' + && contains(github.event.pull_request.labels.*.name, 'ephemeral-environment') runs-on: ubuntu-24.04 needs: build-docker steps: @@ -629,9 +641,8 @@ jobs: steps: - name: Check if any job failed if: | - (github.ref == 'refs/heads/main' - || github.ref == 'refs/heads/rc' - || github.ref == 'refs/heads/hotfix-rc') + github.event_name != 'pull_request_target' + && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc') && contains(needs.*.result, 'failure') run: exit 1 From e7cbdaa4696fc2a781922e12dc177a7366d9370c Mon Sep 17 00:00:00 2001 From: Matt Bishop Date: Fri, 8 Nov 2024 10:31:18 -0500 Subject: [PATCH 12/29] Only build Unified on main branch pushes (#5006) --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0a3489ead..1a915037b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -527,6 +527,7 @@ jobs: self-host-build: name: Trigger self-host build + if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main' runs-on: ubuntu-22.04 needs: build-docker steps: From 7cf67425959481b16fdc95e3fc4ded6e7c8b68a8 Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Fri, 8 Nov 2024 10:28:56 -0600 Subject: [PATCH 13/29] PM-13236 - Password Health Report Application - entities repos (#4974) * PM-13236 PasswordHealthReportApplications db * PM-13236 incorporated pr comments * PM-13236 fixed error in SQL script * PM-13236 resolve quality scan errors SQL71006, SQL7101, SQL70001 * PM-13236 fixed warnings on procedures * PM-13236 added efMigrations * PM-13236 renamed files to PasswordHealthReportApplication (singular) * PM-13236 changed file name to more appropriate naming * PM-13236 changed the file name singular * PM-13236 PasswordHealthReportApplication Entities and Repos * PM-13236 moved files under tools from core * PM-13236 Entity PasswordHealthReportApplication namespace changed to tools/entities * PM-13236 moved Repos and Interfaces to tools * PM-13236 migrated model to tools namespace * PM-13236 minor fixes to the unit tests * PM-13236 fixed script errors during build * PM-13236 Script to drop PasswordHealthReportApplications if it exists * PM-13236 fixes to database snapshot * PM-13236 updated databasesnapshots * PM-13236 Update database model changes for Mysql * PM-13236 update model changes for Sqlite * PM-13236 updated the models to remove commented code * PM-13236 added correct db snapshot for MySql * PM-13236 updated database snapshot for Postgres * PM-13236 updated database snapshot for Sqlite * PM-13236 removed unwanted directive to fix linting error * PM-13236 removed redundant script files --- .../PasswordHealthReportApplication.cs | 6 +- ...sswordHealthReportApplicationRepository.cs | 9 + .../DapperServiceCollectionExtensions.cs | 1 + ...sswordHealthReportApplicationRepository.cs | 33 + .../Repositories/DatabaseContext.cs | 2 + ...eportApplicationEntityTypeConfiguration.cs | 24 + .../Models/PasswordHealthReportApplication.cs | 18 + ...sswordHealthReportApplicationRepository.cs | 30 + .../EntityFrameworkRepositoryFixtures.cs | 2 + ...PasswordHealthReportApplicationFixtures.cs | 82 + ...dHealthReportApplicationRepositoryTests.cs | 269 ++ ...asswordHealthReportApplication.Designer.cs | 2888 ++++++++++++++++ ...5202_FixPasswordHealthReportApplication.cs | 22 + .../DatabaseContextModelSnapshot.cs | 39 + ...asswordHealthReportApplication.Designer.cs | 2894 +++++++++++++++++ ...2053_FixPasswordHealthReportApplication.cs | 22 + .../DatabaseContextModelSnapshot.cs | 39 + ...asswordHealthReportApplication.Designer.cs | 2877 ++++++++++++++++ ...2413_FixPasswordHealthReportApplication.cs | 22 + .../DatabaseContextModelSnapshot.cs | 39 + 20 files changed, 9317 insertions(+), 1 deletion(-) create mode 100644 src/Core/Tools/Repositories/IPasswordHealthReportApplicationRepository.cs create mode 100644 src/Infrastructure.Dapper/Tools/Repositories/PasswordHealthReportApplicationRepository.cs create mode 100644 src/Infrastructure.EntityFramework/Tools/Configurations/PasswordHealthReportApplicationEntityTypeConfiguration.cs create mode 100644 src/Infrastructure.EntityFramework/Tools/Models/PasswordHealthReportApplication.cs create mode 100644 src/Infrastructure.EntityFramework/Tools/Repositories/PasswordHealthReportApplicationRepository.cs create mode 100644 test/Infrastructure.EFIntegration.Test/AutoFixture/PasswordHealthReportApplicationFixtures.cs create mode 100644 test/Infrastructure.EFIntegration.Test/Tools/Repositories/PasswordHealthReportApplicationRepositoryTests.cs create mode 100644 util/MySqlMigrations/Migrations/20241105195202_FixPasswordHealthReportApplication.Designer.cs create mode 100644 util/MySqlMigrations/Migrations/20241105195202_FixPasswordHealthReportApplication.cs create mode 100644 util/PostgresMigrations/Migrations/20241105202053_FixPasswordHealthReportApplication.Designer.cs create mode 100644 util/PostgresMigrations/Migrations/20241105202053_FixPasswordHealthReportApplication.cs create mode 100644 util/SqliteMigrations/Migrations/20241105202413_FixPasswordHealthReportApplication.Designer.cs create mode 100644 util/SqliteMigrations/Migrations/20241105202413_FixPasswordHealthReportApplication.cs diff --git a/src/Core/Tools/Entities/PasswordHealthReportApplication.cs b/src/Core/Tools/Entities/PasswordHealthReportApplication.cs index 51df3a778..9d89edf63 100644 --- a/src/Core/Tools/Entities/PasswordHealthReportApplication.cs +++ b/src/Core/Tools/Entities/PasswordHealthReportApplication.cs @@ -1,11 +1,15 @@ using Bit.Core.Entities; using Bit.Core.Utilities; +#nullable enable + +namespace Bit.Core.Tools.Entities; + public class PasswordHealthReportApplication : ITableObject, IRevisable { public Guid Id { get; set; } public Guid OrganizationId { get; set; } - public string Uri { get; set; } + public string? Uri { get; set; } public DateTime CreationDate { get; set; } = DateTime.UtcNow; public DateTime RevisionDate { get; set; } = DateTime.UtcNow; diff --git a/src/Core/Tools/Repositories/IPasswordHealthReportApplicationRepository.cs b/src/Core/Tools/Repositories/IPasswordHealthReportApplicationRepository.cs new file mode 100644 index 000000000..374f12e12 --- /dev/null +++ b/src/Core/Tools/Repositories/IPasswordHealthReportApplicationRepository.cs @@ -0,0 +1,9 @@ +using Bit.Core.Repositories; +using Bit.Core.Tools.Entities; + +namespace Bit.Core.Tools.Repositories; + +public interface IPasswordHealthReportApplicationRepository : IRepository +{ + Task> GetByOrganizationIdAsync(Guid organizationId); +} diff --git a/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs b/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs index 6cfa1ef8b..77528dc80 100644 --- a/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs +++ b/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs @@ -58,6 +58,7 @@ public static class DapperServiceCollectionExtensions services.AddSingleton(); services .AddSingleton(); + services.AddSingleton(); if (selfHosted) { diff --git a/src/Infrastructure.Dapper/Tools/Repositories/PasswordHealthReportApplicationRepository.cs b/src/Infrastructure.Dapper/Tools/Repositories/PasswordHealthReportApplicationRepository.cs new file mode 100644 index 000000000..0c4599841 --- /dev/null +++ b/src/Infrastructure.Dapper/Tools/Repositories/PasswordHealthReportApplicationRepository.cs @@ -0,0 +1,33 @@ +using System.Data; +using Bit.Core.Settings; +using Bit.Core.Tools.Repositories; +using Bit.Infrastructure.Dapper.Repositories; +using Dapper; +using Microsoft.Data.SqlClient; +using ToolsEntities = Bit.Core.Tools.Entities; + +namespace Bit.Infrastructure.Dapper.Tools.Repositories; + +public class PasswordHealthReportApplicationRepository : Repository, IPasswordHealthReportApplicationRepository +{ + public PasswordHealthReportApplicationRepository(GlobalSettings globalSettings) + : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString) + { } + + public PasswordHealthReportApplicationRepository(string connectionString, string readOnlyConnectionString) + : base(connectionString, readOnlyConnectionString) + { } + + public async Task> GetByOrganizationIdAsync(Guid organizationId) + { + using (var connection = new SqlConnection(ReadOnlyConnectionString)) + { + var results = await connection.QueryAsync( + $"[{Schema}].[PasswordHealthReportApplication_ReadByOrganizationId]", + new { OrganizationId = organizationId }, + commandType: CommandType.StoredProcedure); + + return results.ToList(); + } + } +} diff --git a/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs b/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs index d751b929b..97f004c1f 100644 --- a/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs +++ b/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs @@ -7,6 +7,7 @@ using Bit.Infrastructure.EntityFramework.Converters; using Bit.Infrastructure.EntityFramework.Models; using Bit.Infrastructure.EntityFramework.NotificationCenter.Models; using Bit.Infrastructure.EntityFramework.SecretsManager.Models; +using Bit.Infrastructure.EntityFramework.Tools.Models; using Bit.Infrastructure.EntityFramework.Vault.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -75,6 +76,7 @@ public class DatabaseContext : DbContext public DbSet Notifications { get; set; } public DbSet NotificationStatuses { get; set; } public DbSet ClientOrganizationMigrationRecords { get; set; } + public DbSet PasswordHealthReportApplications { get; set; } protected override void OnModelCreating(ModelBuilder builder) { diff --git a/src/Infrastructure.EntityFramework/Tools/Configurations/PasswordHealthReportApplicationEntityTypeConfiguration.cs b/src/Infrastructure.EntityFramework/Tools/Configurations/PasswordHealthReportApplicationEntityTypeConfiguration.cs new file mode 100644 index 000000000..4f4c5473d --- /dev/null +++ b/src/Infrastructure.EntityFramework/Tools/Configurations/PasswordHealthReportApplicationEntityTypeConfiguration.cs @@ -0,0 +1,24 @@ +using Bit.Infrastructure.EntityFramework.Tools.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Bit.Infrastructure.EntityFramework.Tools.Configurations; + +public class PasswordHealthReportApplicationEntityTypeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder + .Property(s => s.Id) + .ValueGeneratedNever(); + + builder.HasIndex(s => s.Id) + .IsClustered(true); + + builder + .HasIndex(s => s.OrganizationId) + .IsClustered(false); + + builder.ToTable(nameof(PasswordHealthReportApplication)); + } +} diff --git a/src/Infrastructure.EntityFramework/Tools/Models/PasswordHealthReportApplication.cs b/src/Infrastructure.EntityFramework/Tools/Models/PasswordHealthReportApplication.cs new file mode 100644 index 000000000..524cd283c --- /dev/null +++ b/src/Infrastructure.EntityFramework/Tools/Models/PasswordHealthReportApplication.cs @@ -0,0 +1,18 @@ +using AutoMapper; +using Bit.Infrastructure.EntityFramework.AdminConsole.Models; + +namespace Bit.Infrastructure.EntityFramework.Tools.Models; + +public class PasswordHealthReportApplication : Core.Tools.Entities.PasswordHealthReportApplication +{ + public virtual Organization Organization { get; set; } +} + +public class PasswordHealthReportApplicationProfile : Profile +{ + public PasswordHealthReportApplicationProfile() + { + CreateMap() + .ReverseMap(); + } +} diff --git a/src/Infrastructure.EntityFramework/Tools/Repositories/PasswordHealthReportApplicationRepository.cs b/src/Infrastructure.EntityFramework/Tools/Repositories/PasswordHealthReportApplicationRepository.cs new file mode 100644 index 000000000..1d8204a82 --- /dev/null +++ b/src/Infrastructure.EntityFramework/Tools/Repositories/PasswordHealthReportApplicationRepository.cs @@ -0,0 +1,30 @@ +using AutoMapper; +using Bit.Core.Tools.Repositories; +using Bit.Infrastructure.EntityFramework.Repositories; +using Bit.Infrastructure.EntityFramework.Tools.Models; +using LinqToDB; +using Microsoft.Extensions.DependencyInjection; +using AdminConsoleEntities = Bit.Core.Tools.Entities; + +namespace Bit.Infrastructure.EntityFramework.Tools.Repositories; + +public class PasswordHealthReportApplicationRepository : + Repository, + IPasswordHealthReportApplicationRepository +{ + public PasswordHealthReportApplicationRepository(IServiceScopeFactory serviceScopeFactory, + IMapper mapper) : base(serviceScopeFactory, mapper, (DatabaseContext context) => context.PasswordHealthReportApplications) + { } + + public async Task> GetByOrganizationIdAsync(Guid organizationId) + { + using (var scope = ServiceScopeFactory.CreateScope()) + { + var dbContext = GetDatabaseContext(scope); + var results = await dbContext.PasswordHealthReportApplications + .Where(p => p.OrganizationId == organizationId) + .ToListAsync(); + return Mapper.Map>(results); + } + } +} diff --git a/test/Infrastructure.EFIntegration.Test/AutoFixture/EntityFrameworkRepositoryFixtures.cs b/test/Infrastructure.EFIntegration.Test/AutoFixture/EntityFrameworkRepositoryFixtures.cs index ead2d3f98..3775c9953 100644 --- a/test/Infrastructure.EFIntegration.Test/AutoFixture/EntityFrameworkRepositoryFixtures.cs +++ b/test/Infrastructure.EFIntegration.Test/AutoFixture/EntityFrameworkRepositoryFixtures.cs @@ -9,6 +9,7 @@ using Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider; using Bit.Infrastructure.EntityFramework.Auth.Models; using Bit.Infrastructure.EntityFramework.Models; using Bit.Infrastructure.EntityFramework.Repositories; +using Bit.Infrastructure.EntityFramework.Tools.Models; using Bit.Infrastructure.EntityFramework.Vault.Models; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; @@ -89,6 +90,7 @@ public class EfRepositoryListBuilder : ISpecimenBuilder where T : BaseEntityF cfg.AddProfile(); cfg.AddProfile(); cfg.AddProfile(); + cfg.AddProfile(); }) .CreateMapper())); diff --git a/test/Infrastructure.EFIntegration.Test/AutoFixture/PasswordHealthReportApplicationFixtures.cs b/test/Infrastructure.EFIntegration.Test/AutoFixture/PasswordHealthReportApplicationFixtures.cs new file mode 100644 index 000000000..7a1d1bb03 --- /dev/null +++ b/test/Infrastructure.EFIntegration.Test/AutoFixture/PasswordHealthReportApplicationFixtures.cs @@ -0,0 +1,82 @@ +using AutoFixture; +using AutoFixture.Kernel; +using Bit.Core.Tools.Entities; +using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories; +using Bit.Infrastructure.EntityFramework.Repositories; +using Bit.Infrastructure.EntityFramework.Tools.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; + +namespace Bit.Infrastructure.EFIntegration.Test.AutoFixture; + +internal class PasswordHealthReportApplicationBuilder : ISpecimenBuilder +{ + public object Create(object request, ISpecimenContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var type = request as Type; + if (type == null || type != typeof(PasswordHealthReportApplication)) + { + return new NoSpecimen(); + } + + var fixture = new Fixture(); + var obj = fixture.WithAutoNSubstitutions().Create(); + return obj; + } +} + +internal class EfPasswordHealthReportApplication : ICustomization +{ + public void Customize(IFixture fixture) + { + fixture.Customizations.Add(new IgnoreVirtualMembersCustomization()); + fixture.Customizations.Add(new GlobalSettingsBuilder()); + fixture.Customizations.Add(new PasswordHealthReportApplicationBuilder()); + fixture.Customizations.Add(new OrganizationBuilder()); + fixture.Customizations.Add(new EfRepositoryListBuilder()); + fixture.Customizations.Add(new EfRepositoryListBuilder()); + } +} + +internal class EfPasswordHealthReportApplicationApplicableToUser : ICustomization +{ + public void Customize(IFixture fixture) + { + fixture.Customizations.Add(new IgnoreVirtualMembersCustomization()); + fixture.Customizations.Add(new GlobalSettingsBuilder()); + fixture.Customizations.Add(new PasswordHealthReportApplicationBuilder()); + fixture.Customizations.Add(new OrganizationBuilder()); + fixture.Customizations.Add(new EfRepositoryListBuilder()); + fixture.Customizations.Add(new EfRepositoryListBuilder()); + fixture.Customizations.Add(new EfRepositoryListBuilder()); + fixture.Customizations.Add(new EfRepositoryListBuilder()); + fixture.Customizations.Add(new EfRepositoryListBuilder()); + fixture.Customizations.Add(new EfRepositoryListBuilder()); + fixture.Customizations.Add(new EfRepositoryListBuilder()); + } +} + +internal class EfPasswordHealthReportApplicationAutoDataAttribute : CustomAutoDataAttribute +{ + public EfPasswordHealthReportApplicationAutoDataAttribute() : base(new SutProviderCustomization(), new EfPasswordHealthReportApplication()) + { } +} + +internal class EfPasswordHealthReportApplicationApplicableToUserInlineAutoDataAttribute : InlineCustomAutoDataAttribute +{ + public EfPasswordHealthReportApplicationApplicableToUserInlineAutoDataAttribute(params object[] values) : + base(new[] { typeof(SutProviderCustomization), typeof(EfPasswordHealthReportApplicationApplicableToUser) }, values) + { } +} + +internal class InlineEfPasswordHealthReportApplicationAutoDataAttribute : InlineCustomAutoDataAttribute +{ + public InlineEfPasswordHealthReportApplicationAutoDataAttribute(params object[] values) : base(new[] { typeof(SutProviderCustomization), + typeof(EfPolicy) }, values) + { } +} diff --git a/test/Infrastructure.EFIntegration.Test/Tools/Repositories/PasswordHealthReportApplicationRepositoryTests.cs b/test/Infrastructure.EFIntegration.Test/Tools/Repositories/PasswordHealthReportApplicationRepositoryTests.cs new file mode 100644 index 000000000..9c83972a0 --- /dev/null +++ b/test/Infrastructure.EFIntegration.Test/Tools/Repositories/PasswordHealthReportApplicationRepositoryTests.cs @@ -0,0 +1,269 @@ +using AutoFixture; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Repositories; +using Bit.Core.Test.AutoFixture.Attributes; +using Bit.Core.Tools.Entities; +using Bit.Core.Tools.Repositories; +using Bit.Infrastructure.EFIntegration.Test.AutoFixture; +using Xunit; +using EfRepo = Bit.Infrastructure.EntityFramework.Repositories; +using EfToolsRepo = Bit.Infrastructure.EntityFramework.Tools.Repositories; +using SqlAdminConsoleRepo = Bit.Infrastructure.Dapper.Tools.Repositories; +using SqlRepo = Bit.Infrastructure.Dapper.Repositories; + +namespace Bit.Infrastructure.EFIntegration.Test.Tools.Repositories; + +public class PasswordHealthReportApplicationRepositoryTests +{ + [CiSkippedTheory, EfPasswordHealthReportApplicationAutoData] + public async Task CreateAsync_Works_DataMatches( + PasswordHealthReportApplication passwordHealthReportApplication, + Organization organization, + List suts, + List efOrganizationRepos, + SqlAdminConsoleRepo.PasswordHealthReportApplicationRepository sqlPasswordHealthReportApplicationRepo, + SqlRepo.OrganizationRepository sqlOrganizationRepo + ) + { + var passwordHealthReportApplicationRecords = new List(); + foreach (var sut in suts) + { + var i = suts.IndexOf(sut); + + var efOrganization = await efOrganizationRepos[i].CreateAsync(organization); + sut.ClearChangeTracking(); + + passwordHealthReportApplication.OrganizationId = efOrganization.Id; + var postEfPasswordHeathReportApp = await sut.CreateAsync(passwordHealthReportApplication); + sut.ClearChangeTracking(); + + var savedPasswordHealthReportApplication = await sut.GetByIdAsync(postEfPasswordHeathReportApp.Id); + passwordHealthReportApplicationRecords.Add(savedPasswordHealthReportApplication); + } + + var sqlOrganization = await sqlOrganizationRepo.CreateAsync(organization); + + passwordHealthReportApplication.OrganizationId = sqlOrganization.Id; + var sqlPasswordHealthReportApplicationRecord = await sqlPasswordHealthReportApplicationRepo.CreateAsync(passwordHealthReportApplication); + var savedSqlPasswordHealthReportApplicationRecord = await sqlPasswordHealthReportApplicationRepo.GetByIdAsync(sqlPasswordHealthReportApplicationRecord.Id); + passwordHealthReportApplicationRecords.Add(savedSqlPasswordHealthReportApplicationRecord); + + Assert.True(passwordHealthReportApplicationRecords.Count == 4); + } + + [CiSkippedTheory, EfPasswordHealthReportApplicationAutoData] + public async Task RetrieveByOrganisation_Works( + SqlAdminConsoleRepo.PasswordHealthReportApplicationRepository sqlPasswordHealthReportApplicationRepo, + SqlRepo.OrganizationRepository sqlOrganizationRepo) + { + var (firstOrg, firstRecord) = await CreateSampleRecord(sqlOrganizationRepo, sqlPasswordHealthReportApplicationRepo); + var (secondOrg, secondRecord) = await CreateSampleRecord(sqlOrganizationRepo, sqlPasswordHealthReportApplicationRepo); + + var firstSetOfRecords = await sqlPasswordHealthReportApplicationRepo.GetByOrganizationIdAsync(firstOrg.Id); + var nextSetOfRecords = await sqlPasswordHealthReportApplicationRepo.GetByOrganizationIdAsync(secondOrg.Id); + + Assert.True(firstSetOfRecords.Count == 1 && firstSetOfRecords.First().OrganizationId == firstOrg.Id); + Assert.True(nextSetOfRecords.Count == 1 && nextSetOfRecords.First().OrganizationId == secondOrg.Id); + } + + [CiSkippedTheory, EfPasswordHealthReportApplicationAutoData] + public async Task ReplaceQuery_Works( + List suts, + List efOrganizationRepos, + SqlAdminConsoleRepo.PasswordHealthReportApplicationRepository sqlPasswordHealthReportApplicationRepo, + SqlRepo.OrganizationRepository sqlOrganizationRepo) + { + var (org, pwdRecord) = await CreateSampleRecord(sqlOrganizationRepo, sqlPasswordHealthReportApplicationRepo); + var exampleUri = "http://www.example.com"; + var exampleRevisionDate = new DateTime(2021, 1, 1); + var dbRecords = new List(); + + foreach (var sut in suts) + { + var i = suts.IndexOf(sut); + + // create a new organization for each repository + var organization = await efOrganizationRepos[i].CreateAsync(org); + + // map the organization Id and create the PasswordHealthReportApp record + pwdRecord.OrganizationId = organization.Id; + var passwordHealthReportApplication = await sut.CreateAsync(pwdRecord); + + // update the record with new values + passwordHealthReportApplication.Uri = exampleUri; + passwordHealthReportApplication.RevisionDate = exampleRevisionDate; + + // apply update to the database + await sut.ReplaceAsync(passwordHealthReportApplication); + sut.ClearChangeTracking(); + + // retrieve the data and add to the list for assertions + var recordFromDb = await sut.GetByIdAsync(passwordHealthReportApplication.Id); + sut.ClearChangeTracking(); + + dbRecords.Add(recordFromDb); + } + + // sql - create a new organization and PasswordHealthReportApplication record + var (sqlOrg, sqlPwdRecord) = await CreateSampleRecord(sqlOrganizationRepo, sqlPasswordHealthReportApplicationRepo); + var sqlPasswordHealthReportApplicationRecord = await sqlPasswordHealthReportApplicationRepo.GetByIdAsync(sqlPwdRecord.Id); + + // sql - update the record with new values + sqlPasswordHealthReportApplicationRecord.Uri = exampleUri; + sqlPasswordHealthReportApplicationRecord.RevisionDate = exampleRevisionDate; + await sqlPasswordHealthReportApplicationRepo.ReplaceAsync(sqlPasswordHealthReportApplicationRecord); + + // sql - retrieve the data and add to the list for assertions + var sqlDbRecord = await sqlPasswordHealthReportApplicationRepo.GetByIdAsync(sqlPasswordHealthReportApplicationRecord.Id); + dbRecords.Add(sqlDbRecord); + + // assertions + // the Guids must be distinct across all records + Assert.True(dbRecords.Select(_ => _.Id).Distinct().Count() == dbRecords.Count); + + // the Uri and RevisionDate must match the updated values + Assert.True(dbRecords.All(_ => _.Uri == exampleUri && _.RevisionDate == exampleRevisionDate)); + } + + [CiSkippedTheory, EfPasswordHealthReportApplicationAutoData] + public async Task Upsert_Works( + List suts, + List efOrganizationRepos, + SqlAdminConsoleRepo.PasswordHealthReportApplicationRepository sqlPasswordHealthReportApplicationRepo, + SqlRepo.OrganizationRepository sqlOrganizationRepo) + { + var fixture = new Fixture(); + var rawOrg = fixture.Build().Create(); + var rawPwdRecord = fixture.Build() + .With(_ => _.OrganizationId, rawOrg.Id) + .Without(_ => _.Id) + .Create(); + var exampleUri = "http://www.example.com"; + var exampleRevisionDate = new DateTime(2021, 1, 1); + var dbRecords = new List(); + + foreach (var sut in suts) + { + var i = suts.IndexOf(sut); + + // create a new organization for each repository + var organization = await efOrganizationRepos[i].CreateAsync(rawOrg); + + // map the organization Id and use Upsert to save new record + rawPwdRecord.OrganizationId = organization.Id; + rawPwdRecord.Id = default(Guid); + await sut.UpsertAsync(rawPwdRecord); + sut.ClearChangeTracking(); + + // retrieve the data and add to the list for assertions + var passwordHealthReportApplication = await sut.GetByIdAsync(rawPwdRecord.Id); + + // update the record with new values + passwordHealthReportApplication.Uri = exampleUri; + passwordHealthReportApplication.RevisionDate = exampleRevisionDate; + + // apply update using Upsert to make changes to db + await sut.UpsertAsync(passwordHealthReportApplication); + sut.ClearChangeTracking(); + + // retrieve the data and add to the list for assertions + var recordFromDb = await sut.GetByIdAsync(passwordHealthReportApplication.Id); + dbRecords.Add(recordFromDb); + + sut.ClearChangeTracking(); + } + + // sql - create new records + var organizationForSql = fixture.Create(); + var passwordHealthReportApplicationForSql = fixture.Build() + .With(_ => _.OrganizationId, organizationForSql.Id) + .Without(_ => _.Id) + .Create(); + + // sql - use Upsert to insert this data + var sqlOrganization = await sqlOrganizationRepo.CreateAsync(organizationForSql); + await sqlPasswordHealthReportApplicationRepo.UpsertAsync(passwordHealthReportApplicationForSql); + var sqlPasswordHealthReportApplicationRecord = await sqlPasswordHealthReportApplicationRepo.GetByIdAsync(passwordHealthReportApplicationForSql.Id); + + // sql - update the record with new values + sqlPasswordHealthReportApplicationRecord.Uri = exampleUri; + sqlPasswordHealthReportApplicationRecord.RevisionDate = exampleRevisionDate; + await sqlPasswordHealthReportApplicationRepo.UpsertAsync(sqlPasswordHealthReportApplicationRecord); + + // sql - retrieve the data and add to the list for assertions + var sqlDbRecord = await sqlPasswordHealthReportApplicationRepo.GetByIdAsync(sqlPasswordHealthReportApplicationRecord.Id); + dbRecords.Add(sqlDbRecord); + + // assertions + // the Guids must be distinct across all records + Assert.True(dbRecords.Select(_ => _.Id).Distinct().Count() == dbRecords.Count); + + // the Uri and RevisionDate must match the updated values + Assert.True(dbRecords.All(_ => _.Uri == exampleUri && _.RevisionDate == exampleRevisionDate)); + } + + [CiSkippedTheory, EfPasswordHealthReportApplicationAutoData] + public async Task Delete_Works( + List suts, + List efOrganizationRepos, + SqlAdminConsoleRepo.PasswordHealthReportApplicationRepository sqlPasswordHealthReportApplicationRepo, + SqlRepo.OrganizationRepository sqlOrganizationRepo) + { + var fixture = new Fixture(); + var rawOrg = fixture.Build().Create(); + var rawPwdRecord = fixture.Build() + .With(_ => _.OrganizationId, rawOrg.Id) + .Create(); + var dbRecords = new List(); + + foreach (var sut in suts) + { + var i = suts.IndexOf(sut); + + // create a new organization for each repository + var organization = await efOrganizationRepos[i].CreateAsync(rawOrg); + + // map the organization Id and use Upsert to save new record + rawPwdRecord.OrganizationId = organization.Id; + rawPwdRecord = await sut.CreateAsync(rawPwdRecord); + sut.ClearChangeTracking(); + + // apply update using Upsert to make changes to db + await sut.DeleteAsync(rawPwdRecord); + sut.ClearChangeTracking(); + + // retrieve the data and add to the list for assertions + var recordFromDb = await sut.GetByIdAsync(rawPwdRecord.Id); + dbRecords.Add(recordFromDb); + + sut.ClearChangeTracking(); + } + + // sql - create new records + var (org, passwordHealthReportApplication) = await CreateSampleRecord(sqlOrganizationRepo, sqlPasswordHealthReportApplicationRepo); + await sqlPasswordHealthReportApplicationRepo.DeleteAsync(passwordHealthReportApplication); + var sqlDbRecord = await sqlPasswordHealthReportApplicationRepo.GetByIdAsync(passwordHealthReportApplication.Id); + dbRecords.Add(sqlDbRecord); + + // assertions + // all records should be null - as they were deleted before querying + Assert.True(dbRecords.Where(_ => _ == null).Count() == 4); + } + + private async Task<(Organization, PasswordHealthReportApplication)> CreateSampleRecord( + IOrganizationRepository organizationRepo, + IPasswordHealthReportApplicationRepository passwordHealthReportApplicationRepo + ) + { + var fixture = new Fixture(); + var organization = fixture.Create(); + var passwordHealthReportApplication = fixture.Build() + .With(_ => _.OrganizationId, organization.Id) + .Create(); + + organization = await organizationRepo.CreateAsync(organization); + passwordHealthReportApplication = await passwordHealthReportApplicationRepo.CreateAsync(passwordHealthReportApplication); + + return (organization, passwordHealthReportApplication); + } +} diff --git a/util/MySqlMigrations/Migrations/20241105195202_FixPasswordHealthReportApplication.Designer.cs b/util/MySqlMigrations/Migrations/20241105195202_FixPasswordHealthReportApplication.Designer.cs new file mode 100644 index 000000000..eb94e3812 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20241105195202_FixPasswordHealthReportApplication.Designer.cs @@ -0,0 +1,2888 @@ +// +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("20241105195202_FixPasswordHealthReportApplication")] + partial class FixPasswordHealthReportApplication + { + /// + 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.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("LimitCollectionCreationDeletion") + .HasColumnType("tinyint(1)"); + + b.Property("LimitCollectionDeletion") + .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("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("Use2fa") + .HasColumnType("tinyint(1)"); + + b.Property("UseApi") + .HasColumnType("tinyint(1)"); + + b.Property("UseCustomPermissions") + .HasColumnType("tinyint(1)"); + + b.Property("UseDirectory") + .HasColumnType("tinyint(1)"); + + b.Property("UseEvents") + .HasColumnType("tinyint(1)"); + + b.Property("UseGroups") + .HasColumnType("tinyint(1)"); + + b.Property("UseKeyConnector") + .HasColumnType("tinyint(1)"); + + b.Property("UsePasswordManager") + .HasColumnType("tinyint(1)"); + + b.Property("UsePolicies") + .HasColumnType("tinyint(1)"); + + b.Property("UseResetPassword") + .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" }); + + 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("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("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("int"); + + 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(50) + .HasColumnType("varchar(50)"); + + 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.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.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("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.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("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("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"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + 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.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.HasKey("Id"); + + b.ToTable("Installation", (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("LastSyncDate") + .HasColumnType("datetime(6)"); + + 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.Send", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessCount") + .HasColumnType("int"); + + 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("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("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("varchar(32)"); + + b.Property("UsesKeyConnector") + .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.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Body") + .HasColumnType("longtext"); + + 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("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("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.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.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.Tools.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.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + 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("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.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.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.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.NotificationCenter.Models.Notification", 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.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.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.Tools.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.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("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("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/20241105195202_FixPasswordHealthReportApplication.cs b/util/MySqlMigrations/Migrations/20241105195202_FixPasswordHealthReportApplication.cs new file mode 100644 index 000000000..fb7fd5f63 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20241105195202_FixPasswordHealthReportApplication.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations; + +/// +public partial class FixPasswordHealthReportApplication : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + // this file is not required, but the designer file is required + // in order to keep the database models in sync with the database + // without this - the unit tests will fail when run on your local machine + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + } +} diff --git a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs index be1369b02..23792e47e 100644 --- a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -1887,6 +1887,34 @@ namespace Bit.MySqlMigrations.Migrations b.ToTable("ServiceAccount", (string)null); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.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.Vault.Models.Cipher", b => { b.Property("Id") @@ -2578,6 +2606,17 @@ namespace Bit.MySqlMigrations.Migrations b.Navigation("Organization"); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.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.Vault.Models.Cipher", b => { b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") diff --git a/util/PostgresMigrations/Migrations/20241105202053_FixPasswordHealthReportApplication.Designer.cs b/util/PostgresMigrations/Migrations/20241105202053_FixPasswordHealthReportApplication.Designer.cs new file mode 100644 index 000000000..0310007ec --- /dev/null +++ b/util/PostgresMigrations/Migrations/20241105202053_FixPasswordHealthReportApplication.Designer.cs @@ -0,0 +1,2894 @@ +// +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("20241105202053_FixPasswordHealthReportApplication")] + partial class FixPasswordHealthReportApplication + { + /// + 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.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("LimitCollectionCreationDeletion") + .HasColumnType("boolean"); + + b.Property("LimitCollectionDeletion") + .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("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("Use2fa") + .HasColumnType("boolean"); + + b.Property("UseApi") + .HasColumnType("boolean"); + + b.Property("UseCustomPermissions") + .HasColumnType("boolean"); + + b.Property("UseDirectory") + .HasColumnType("boolean"); + + b.Property("UseEvents") + .HasColumnType("boolean"); + + b.Property("UseGroups") + .HasColumnType("boolean"); + + b.Property("UseKeyConnector") + .HasColumnType("boolean"); + + b.Property("UsePasswordManager") + .HasColumnType("boolean"); + + b.Property("UsePolicies") + .HasColumnType("boolean"); + + b.Property("UseResetPassword") + .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" }); + + 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("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("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("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"); + + 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(50) + .HasColumnType("character varying(50)") + .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.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.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("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.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("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("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"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + 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.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.HasKey("Id"); + + b.ToTable("Installation", (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("LastSyncDate") + .HasColumnType("timestamp with time zone"); + + 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.Send", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessCount") + .HasColumnType("integer"); + + 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("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("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("UsesKeyConnector") + .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.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Body") + .HasColumnType("text"); + + 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("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("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.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.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.Tools.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.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + 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("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.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.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.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.NotificationCenter.Models.Notification", 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.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.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.Tools.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.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("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("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/20241105202053_FixPasswordHealthReportApplication.cs b/util/PostgresMigrations/Migrations/20241105202053_FixPasswordHealthReportApplication.cs new file mode 100644 index 000000000..cf8087b78 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20241105202053_FixPasswordHealthReportApplication.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations; + +/// +public partial class FixPasswordHealthReportApplication : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + // this file is not required, but the designer file is required + // in order to keep the database models in sync with the database + // without this - the unit tests will fail when run on your local machine + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + } +} diff --git a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs index 659f42538..e344e7663 100644 --- a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -1893,6 +1893,34 @@ namespace Bit.PostgresMigrations.Migrations b.ToTable("ServiceAccount", (string)null); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.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.Vault.Models.Cipher", b => { b.Property("Id") @@ -2584,6 +2612,17 @@ namespace Bit.PostgresMigrations.Migrations b.Navigation("Organization"); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.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.Vault.Models.Cipher", b => { b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") diff --git a/util/SqliteMigrations/Migrations/20241105202413_FixPasswordHealthReportApplication.Designer.cs b/util/SqliteMigrations/Migrations/20241105202413_FixPasswordHealthReportApplication.Designer.cs new file mode 100644 index 000000000..2cd4ba73f --- /dev/null +++ b/util/SqliteMigrations/Migrations/20241105202413_FixPasswordHealthReportApplication.Designer.cs @@ -0,0 +1,2877 @@ +// +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("20241105202413_FixPasswordHealthReportApplication")] + partial class FixPasswordHealthReportApplication + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.8"); + + 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("LimitCollectionCreationDeletion") + .HasColumnType("INTEGER"); + + b.Property("LimitCollectionDeletion") + .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("TwoFactorProviders") + .HasColumnType("TEXT"); + + b.Property("Use2fa") + .HasColumnType("INTEGER"); + + b.Property("UseApi") + .HasColumnType("INTEGER"); + + b.Property("UseCustomPermissions") + .HasColumnType("INTEGER"); + + b.Property("UseDirectory") + .HasColumnType("INTEGER"); + + b.Property("UseEvents") + .HasColumnType("INTEGER"); + + b.Property("UseGroups") + .HasColumnType("INTEGER"); + + b.Property("UseKeyConnector") + .HasColumnType("INTEGER"); + + b.Property("UsePasswordManager") + .HasColumnType("INTEGER"); + + b.Property("UsePolicies") + .HasColumnType("INTEGER"); + + b.Property("UseResetPassword") + .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" }); + + 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("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("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(50) + .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.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.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("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + 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("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("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"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + 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.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.HasKey("Id"); + + b.ToTable("Installation", (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("LastSyncDate") + .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.Send", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessCount") + .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("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("Storage") + .HasColumnType("INTEGER"); + + b.Property("TwoFactorProviders") + .HasColumnType("TEXT"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UsesKeyConnector") + .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.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Body") + .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("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("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.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.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.Tools.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.Vault.Models.Cipher", b => + { + b.Property("Id") + .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("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.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.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.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.NotificationCenter.Models.Notification", 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.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.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.Tools.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.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("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("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/20241105202413_FixPasswordHealthReportApplication.cs b/util/SqliteMigrations/Migrations/20241105202413_FixPasswordHealthReportApplication.cs new file mode 100644 index 000000000..7a16f1710 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20241105202413_FixPasswordHealthReportApplication.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations; + +/// +public partial class FixPasswordHealthReportApplication : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + // this file is not required, but the designer file is required + // in order to keep the database models in sync with the database + // without this - the unit tests will fail when run on your local machine + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + } +} diff --git a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs index a7eb51d68..9d7902abf 100644 --- a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -1876,6 +1876,34 @@ namespace Bit.SqliteMigrations.Migrations b.ToTable("ServiceAccount", (string)null); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.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.Vault.Models.Cipher", b => { b.Property("Id") @@ -2567,6 +2595,17 @@ namespace Bit.SqliteMigrations.Migrations b.Navigation("Organization"); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.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.Vault.Models.Cipher", b => { b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") From a56f3a587cc05fd35bd132430cad1f105bf18035 Mon Sep 17 00:00:00 2001 From: MtnBurrit0 <77340197+mimartin12@users.noreply.github.com> Date: Fri, 8 Nov 2024 11:58:07 -0700 Subject: [PATCH 14/29] Update logic to handle pull_request_target (#5008) - Removing the grep and create a conditional based on GITHUB_EVENT_NAME --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1a915037b..f428ede5f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -224,7 +224,7 @@ jobs: - name: Generate Docker image tag id: tag run: | - if [[ $(grep "pull" <<< "${GITHUB_REF}") ]]; then + if [[ "${GITHUB_EVENT_NAME}" == "pull_request_target" ]]; then IMAGE_TAG=$(echo "${GITHUB_HEAD_REF}" | sed "s#/#-#g") else IMAGE_TAG=$(echo "${GITHUB_REF:11}" | sed "s#/#-#g") From aa3d71607f20cc21c30f0fdd37a9cbf94c529219 Mon Sep 17 00:00:00 2001 From: Jimmy Vo Date: Fri, 8 Nov 2024 15:02:51 -0500 Subject: [PATCH 15/29] PM-13763 Move ResetPasswordEnrolled to response model (#4983) to adhere to Liskov Substitution Principle. Ensures request models inherit only relevant properties. --- .../Public/Models/MemberBaseModel.cs | 8 +--- .../Models/Response/MemberResponseModel.cs | 8 ++++ .../Response/MemberResponseModelTests.cs | 41 +++++++++++++++++++ 3 files changed, 50 insertions(+), 7 deletions(-) create mode 100644 test/Api.Test/AdminConsole/Public/Models/Response/MemberResponseModelTests.cs diff --git a/src/Api/AdminConsole/Public/Models/MemberBaseModel.cs b/src/Api/AdminConsole/Public/Models/MemberBaseModel.cs index 931f63741..c56117ae7 100644 --- a/src/Api/AdminConsole/Public/Models/MemberBaseModel.cs +++ b/src/Api/AdminConsole/Public/Models/MemberBaseModel.cs @@ -18,7 +18,6 @@ public abstract class MemberBaseModel Type = user.Type; ExternalId = user.ExternalId; - ResetPasswordEnrolled = user.ResetPasswordKey != null; if (Type == OrganizationUserType.Custom) { @@ -35,7 +34,6 @@ public abstract class MemberBaseModel Type = user.Type; ExternalId = user.ExternalId; - ResetPasswordEnrolled = user.ResetPasswordKey != null; if (Type == OrganizationUserType.Custom) { @@ -55,11 +53,7 @@ public abstract class MemberBaseModel /// external_id_123456 [StringLength(300)] public string ExternalId { get; set; } - /// - /// Returns true if the member has enrolled in Password Reset assistance within the organization - /// - [Required] - public bool ResetPasswordEnrolled { get; set; } + /// /// The member's custom permissions if the member has a Custom role. If not supplied, all custom permissions will /// default to false. diff --git a/src/Api/AdminConsole/Public/Models/Response/MemberResponseModel.cs b/src/Api/AdminConsole/Public/Models/Response/MemberResponseModel.cs index 6f73532ad..ab6ecbca4 100644 --- a/src/Api/AdminConsole/Public/Models/Response/MemberResponseModel.cs +++ b/src/Api/AdminConsole/Public/Models/Response/MemberResponseModel.cs @@ -28,6 +28,7 @@ public class MemberResponseModel : MemberBaseModel, IResponseModel Email = user.Email; Status = user.Status; Collections = collections?.Select(c => new AssociationWithPermissionsResponseModel(c)); + ResetPasswordEnrolled = user.ResetPasswordKey != null; } public MemberResponseModel(OrganizationUserUserDetails user, bool twoFactorEnabled, @@ -45,6 +46,7 @@ public class MemberResponseModel : MemberBaseModel, IResponseModel TwoFactorEnabled = twoFactorEnabled; Status = user.Status; Collections = collections?.Select(c => new AssociationWithPermissionsResponseModel(c)); + ResetPasswordEnrolled = user.ResetPasswordKey != null; } /// @@ -93,4 +95,10 @@ public class MemberResponseModel : MemberBaseModel, IResponseModel /// The associated collections that this member can access. /// public IEnumerable Collections { get; set; } + + /// + /// Returns true if the member has enrolled in Password Reset assistance within the organization + /// + [Required] + public bool ResetPasswordEnrolled { get; } } diff --git a/test/Api.Test/AdminConsole/Public/Models/Response/MemberResponseModelTests.cs b/test/Api.Test/AdminConsole/Public/Models/Response/MemberResponseModelTests.cs new file mode 100644 index 000000000..a9193258b --- /dev/null +++ b/test/Api.Test/AdminConsole/Public/Models/Response/MemberResponseModelTests.cs @@ -0,0 +1,41 @@ +using Bit.Api.AdminConsole.Public.Models.Response; +using Bit.Core.Entities; +using Bit.Core.Models.Data; +using NSubstitute; +using Xunit; + +namespace Bit.Api.Test.AdminConsole.Public.Models.Response; + + +public class MemberResponseModelTests +{ + [Fact] + public void ResetPasswordEnrolled_ShouldBeTrue_WhenUserHasResetPasswordKey() + { + // Arrange + var user = Substitute.For(); + var collections = Substitute.For>(); + user.ResetPasswordKey = "none-empty"; + + + // Act + var sut = new MemberResponseModel(user, collections); + + // Assert + Assert.True(sut.ResetPasswordEnrolled); + } + + [Fact] + public void ResetPasswordEnrolled_ShouldBeFalse_WhenUserDoesNotHaveResetPasswordKey() + { + // Arrange + var user = Substitute.For(); + var collections = Substitute.For>(); + + // Act + var sut = new MemberResponseModel(user, collections); + + // Assert + Assert.False(sut.ResetPasswordEnrolled); + } +} From 89be2f495abcc1d10e6f1b46a4d6348ffc68c88c Mon Sep 17 00:00:00 2001 From: Alex Urbina <42731074+urbinaalex17@users.noreply.github.com> Date: Fri, 8 Nov 2024 14:41:58 -0600 Subject: [PATCH 16/29] Fix Hackerone Report ID 2830741 (#5010) --- .github/workflows/build.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f428ede5f..f83b03b16 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -36,7 +36,9 @@ jobs: build-artifacts: name: Build artifacts runs-on: ubuntu-22.04 - needs: lint + needs: + - lint + - check-run strategy: fail-fast: false matrix: From 2e635c950532ff9c116a07b190dea9449aa24ea8 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Mon, 11 Nov 2024 10:02:21 -0500 Subject: [PATCH 17/29] Create ProviderInvoiceItems for empty invoices (#5021) --- .../Implementations/ProviderEventService.cs | 46 ++++++++----------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/src/Billing/Services/Implementations/ProviderEventService.cs b/src/Billing/Services/Implementations/ProviderEventService.cs index e716f8f16..7ce72526c 100644 --- a/src/Billing/Services/Implementations/ProviderEventService.cs +++ b/src/Billing/Services/Implementations/ProviderEventService.cs @@ -94,20 +94,17 @@ public class ProviderEventService( var unassignedEnterpriseSeats = enterpriseProviderPlan.SeatMinimum - enterpriseClientSeats ?? 0; - if (unassignedEnterpriseSeats > 0) + invoiceItems.Add(new ProviderInvoiceItem { - invoiceItems.Add(new ProviderInvoiceItem - { - ProviderId = parsedProviderId, - InvoiceId = invoice.Id, - InvoiceNumber = invoice.Number, - ClientName = "Unassigned seats", - PlanName = enterprisePlan.Name, - AssignedSeats = unassignedEnterpriseSeats, - UsedSeats = 0, - Total = unassignedEnterpriseSeats * discountedEnterpriseSeatPrice - }); - } + ProviderId = parsedProviderId, + InvoiceId = invoice.Id, + InvoiceNumber = invoice.Number, + ClientName = "Unassigned seats", + PlanName = enterprisePlan.Name, + AssignedSeats = unassignedEnterpriseSeats, + UsedSeats = 0, + Total = unassignedEnterpriseSeats * discountedEnterpriseSeatPrice + }); } if (teamsProviderPlan.PurchasedSeats is null or 0) @@ -118,20 +115,17 @@ public class ProviderEventService( var unassignedTeamsSeats = teamsProviderPlan.SeatMinimum - teamsClientSeats ?? 0; - if (unassignedTeamsSeats > 0) + invoiceItems.Add(new ProviderInvoiceItem { - invoiceItems.Add(new ProviderInvoiceItem - { - ProviderId = parsedProviderId, - InvoiceId = invoice.Id, - InvoiceNumber = invoice.Number, - ClientName = "Unassigned seats", - PlanName = teamsPlan.Name, - AssignedSeats = unassignedTeamsSeats, - UsedSeats = 0, - Total = unassignedTeamsSeats * discountedTeamsSeatPrice - }); - } + ProviderId = parsedProviderId, + InvoiceId = invoice.Id, + InvoiceNumber = invoice.Number, + ClientName = "Unassigned seats", + PlanName = teamsPlan.Name, + AssignedSeats = unassignedTeamsSeats, + UsedSeats = 0, + Total = unassignedTeamsSeats * discountedTeamsSeatPrice + }); } await Task.WhenAll(invoiceItems.Select(providerInvoiceItemRepository.CreateAsync)); From 1dec51bf5affedc68d3ad6f98a1f0236eaaa60d8 Mon Sep 17 00:00:00 2001 From: Jared McCannon Date: Mon, 11 Nov 2024 09:52:42 -0600 Subject: [PATCH 18/29] [PM-13014] - Add CanToggleStatus property to PolicyRepsonseModel based on Policy Validators (#4940) * Adding CanToggleState to PoliciesControllers (api/public) endpoints. Added mappings wrapped in feature flag. * Updated logic for determining CanToggle. Removed setting of toggle from List endpoint. Added new details model for single policy response. Validator now returns after first error. --- .../Controllers/PoliciesController.cs | 30 +++++--- .../Response/Helpers/PolicyDetailResponses.cs | 19 +++++ .../PolicyDetailResponseModel.cs | 20 ++++++ .../Organizations}/PolicyResponseModel.cs | 2 +- .../Public/Controllers/PoliciesController.cs | 12 ++-- .../Controllers/EmergencyAccessController.cs | 2 +- .../Models/Response/SyncResponseModel.cs | 4 +- .../VerifyOrganizationDomainCommand.cs | 3 - .../Implementations/SavePolicyCommand.cs | 3 +- .../SingleOrgPolicyValidator.cs | 25 ++++++- .../Services/Implementations/PolicyService.cs | 2 +- .../Helpers/PolicyDetailResponsesTests.cs | 69 +++++++++++++++++++ .../Controllers/PoliciesControllerTests.cs | 6 +- .../Services/PolicyServiceTests.cs | 2 +- 14 files changed, 167 insertions(+), 32 deletions(-) create mode 100644 src/Api/AdminConsole/Models/Response/Helpers/PolicyDetailResponses.cs create mode 100644 src/Api/AdminConsole/Models/Response/Organizations/PolicyDetailResponseModel.cs rename src/{Core/AdminConsole/Models/Api/Response => Api/AdminConsole/Models/Response/Organizations}/PolicyResponseModel.cs (93%) create mode 100644 test/Api.Test/AdminConsole/Models/Response/Helpers/PolicyDetailResponsesTests.cs diff --git a/src/Api/AdminConsole/Controllers/PoliciesController.cs b/src/Api/AdminConsole/Controllers/PoliciesController.cs index 4a1becc0b..ee48cdd5d 100644 --- a/src/Api/AdminConsole/Controllers/PoliciesController.cs +++ b/src/Api/AdminConsole/Controllers/PoliciesController.cs @@ -1,7 +1,11 @@ using Bit.Api.AdminConsole.Models.Request; +using Bit.Api.AdminConsole.Models.Response.Helpers; +using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Api.Models.Response; +using Bit.Core; +using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; -using Bit.Core.AdminConsole.Models.Api.Response; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Models.Business.Tokenables; @@ -16,7 +20,6 @@ using Bit.Core.Utilities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Mvc; -using AdminConsoleEntities = Bit.Core.AdminConsole.Entities; namespace Bit.Api.AdminConsole.Controllers; @@ -32,6 +35,8 @@ public class PoliciesController : Controller private readonly GlobalSettings _globalSettings; private readonly IDataProtector _organizationServiceDataProtector; private readonly IDataProtectorTokenFactory _orgUserInviteTokenDataFactory; + private readonly IFeatureService _featureService; + private readonly IOrganizationHasVerifiedDomainsQuery _organizationHasVerifiedDomainsQuery; public PoliciesController( IPolicyRepository policyRepository, @@ -41,7 +46,9 @@ public class PoliciesController : Controller ICurrentContext currentContext, GlobalSettings globalSettings, IDataProtectionProvider dataProtectionProvider, - IDataProtectorTokenFactory orgUserInviteTokenDataFactory) + IDataProtectorTokenFactory orgUserInviteTokenDataFactory, + IFeatureService featureService, + IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery) { _policyRepository = policyRepository; _policyService = policyService; @@ -53,10 +60,12 @@ public class PoliciesController : Controller "OrganizationServiceDataProtector"); _orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory; + _featureService = featureService; + _organizationHasVerifiedDomainsQuery = organizationHasVerifiedDomainsQuery; } [HttpGet("{type}")] - public async Task Get(Guid orgId, int type) + public async Task Get(Guid orgId, int type) { if (!await _currentContext.ManagePolicies(orgId)) { @@ -65,10 +74,15 @@ public class PoliciesController : Controller var policy = await _policyRepository.GetByOrganizationIdTypeAsync(orgId, (PolicyType)type); if (policy == null) { - return new PolicyResponseModel(new AdminConsoleEntities.Policy() { Type = (PolicyType)type, Enabled = false }); + return new PolicyDetailResponseModel(new Policy { Type = (PolicyType)type }); } - return new PolicyResponseModel(policy); + if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) && policy.Type is PolicyType.SingleOrg) + { + return await policy.GetSingleOrgPolicyDetailResponseAsync(_organizationHasVerifiedDomainsQuery); + } + + return new PolicyDetailResponseModel(policy); } [HttpGet("")] @@ -81,8 +95,8 @@ public class PoliciesController : Controller } var policies = await _policyRepository.GetManyByOrganizationIdAsync(orgIdGuid); - var responses = policies.Select(p => new PolicyResponseModel(p)); - return new ListResponseModel(responses); + + return new ListResponseModel(policies.Select(p => new PolicyResponseModel(p))); } [AllowAnonymous] diff --git a/src/Api/AdminConsole/Models/Response/Helpers/PolicyDetailResponses.cs b/src/Api/AdminConsole/Models/Response/Helpers/PolicyDetailResponses.cs new file mode 100644 index 000000000..14b9642f6 --- /dev/null +++ b/src/Api/AdminConsole/Models/Response/Helpers/PolicyDetailResponses.cs @@ -0,0 +1,19 @@ +using Bit.Api.AdminConsole.Models.Response.Organizations; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; + +namespace Bit.Api.AdminConsole.Models.Response.Helpers; + +public static class PolicyDetailResponses +{ + public static async Task GetSingleOrgPolicyDetailResponseAsync(this Policy policy, IOrganizationHasVerifiedDomainsQuery hasVerifiedDomainsQuery) + { + if (policy.Type is not PolicyType.SingleOrg) + { + throw new ArgumentException($"'{nameof(policy)}' must be of type '{nameof(PolicyType.SingleOrg)}'.", nameof(policy)); + } + + return new PolicyDetailResponseModel(policy, !await hasVerifiedDomainsQuery.HasVerifiedDomainsAsync(policy.OrganizationId)); + } +} diff --git a/src/Api/AdminConsole/Models/Response/Organizations/PolicyDetailResponseModel.cs b/src/Api/AdminConsole/Models/Response/Organizations/PolicyDetailResponseModel.cs new file mode 100644 index 000000000..cb5560e68 --- /dev/null +++ b/src/Api/AdminConsole/Models/Response/Organizations/PolicyDetailResponseModel.cs @@ -0,0 +1,20 @@ +using Bit.Core.AdminConsole.Entities; + +namespace Bit.Api.AdminConsole.Models.Response.Organizations; + +public class PolicyDetailResponseModel : PolicyResponseModel +{ + public PolicyDetailResponseModel(Policy policy, string obj = "policy") : base(policy, obj) + { + } + + public PolicyDetailResponseModel(Policy policy, bool canToggleState) : base(policy) + { + CanToggleState = canToggleState; + } + + /// + /// Indicates whether the Policy can be enabled/disabled + /// + public bool CanToggleState { get; set; } = true; +} diff --git a/src/Core/AdminConsole/Models/Api/Response/PolicyResponseModel.cs b/src/Api/AdminConsole/Models/Response/Organizations/PolicyResponseModel.cs similarity index 93% rename from src/Core/AdminConsole/Models/Api/Response/PolicyResponseModel.cs rename to src/Api/AdminConsole/Models/Response/Organizations/PolicyResponseModel.cs index 7ef6b1573..86e62a419 100644 --- a/src/Core/AdminConsole/Models/Api/Response/PolicyResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/Organizations/PolicyResponseModel.cs @@ -3,7 +3,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.Models.Api; -namespace Bit.Core.AdminConsole.Models.Api.Response; +namespace Bit.Api.AdminConsole.Models.Response.Organizations; public class PolicyResponseModel : ResponseModel { diff --git a/src/Api/AdminConsole/Public/Controllers/PoliciesController.cs b/src/Api/AdminConsole/Public/Controllers/PoliciesController.cs index 71e03a547..f2e7c35d2 100644 --- a/src/Api/AdminConsole/Public/Controllers/PoliciesController.cs +++ b/src/Api/AdminConsole/Public/Controllers/PoliciesController.cs @@ -41,14 +41,13 @@ public class PoliciesController : Controller [ProducesResponseType((int)HttpStatusCode.NotFound)] public async Task Get(PolicyType type) { - var policy = await _policyRepository.GetByOrganizationIdTypeAsync( - _currentContext.OrganizationId.Value, type); + var policy = await _policyRepository.GetByOrganizationIdTypeAsync(_currentContext.OrganizationId.Value, type); if (policy == null) { return new NotFoundResult(); } - var response = new PolicyResponseModel(policy); - return new JsonResult(response); + + return new JsonResult(new PolicyResponseModel(policy)); } /// @@ -62,9 +61,8 @@ public class PoliciesController : Controller public async Task List() { var policies = await _policyRepository.GetManyByOrganizationIdAsync(_currentContext.OrganizationId.Value); - var policyResponses = policies.Select(p => new PolicyResponseModel(p)); - var response = new ListResponseModel(policyResponses); - return new JsonResult(response); + + return new JsonResult(new ListResponseModel(policies.Select(p => new PolicyResponseModel(p)))); } /// diff --git a/src/Api/Auth/Controllers/EmergencyAccessController.cs b/src/Api/Auth/Controllers/EmergencyAccessController.cs index 95fac234c..9f8ea3df0 100644 --- a/src/Api/Auth/Controllers/EmergencyAccessController.cs +++ b/src/Api/Auth/Controllers/EmergencyAccessController.cs @@ -1,10 +1,10 @@ using Bit.Api.AdminConsole.Models.Request.Organizations; +using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Api.Auth.Models.Request; using Bit.Api.Auth.Models.Response; using Bit.Api.Models.Response; using Bit.Api.Vault.Models.Response; using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.Models.Api.Response; using Bit.Core.Auth.Services; using Bit.Core.Exceptions; using Bit.Core.Repositories; diff --git a/src/Api/Vault/Models/Response/SyncResponseModel.cs b/src/Api/Vault/Models/Response/SyncResponseModel.cs index ce5f4562d..a9b87ac31 100644 --- a/src/Api/Vault/Models/Response/SyncResponseModel.cs +++ b/src/Api/Vault/Models/Response/SyncResponseModel.cs @@ -1,7 +1,7 @@ -using Bit.Api.Models.Response; +using Bit.Api.AdminConsole.Models.Response.Organizations; +using Bit.Api.Models.Response; using Bit.Api.Tools.Models.Response; using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.Models.Api.Response; using Bit.Core.AdminConsole.Models.Data.Provider; using Bit.Core.Entities; using Bit.Core.Models.Api; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommand.cs index 4a597a290..870fa72aa 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommand.cs @@ -20,7 +20,6 @@ public class VerifyOrganizationDomainCommand : IVerifyOrganizationDomainCommand private readonly IGlobalSettings _globalSettings; private readonly IPolicyService _policyService; private readonly IFeatureService _featureService; - private readonly IOrganizationService _organizationService; private readonly ILogger _logger; public VerifyOrganizationDomainCommand( @@ -30,7 +29,6 @@ public class VerifyOrganizationDomainCommand : IVerifyOrganizationDomainCommand IGlobalSettings globalSettings, IPolicyService policyService, IFeatureService featureService, - IOrganizationService organizationService, ILogger logger) { _organizationDomainRepository = organizationDomainRepository; @@ -39,7 +37,6 @@ public class VerifyOrganizationDomainCommand : IVerifyOrganizationDomainCommand _globalSettings = globalSettings; _policyService = policyService; _featureService = featureService; - _organizationService = organizationService; _logger = logger; } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/SavePolicyCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/SavePolicyCommand.cs index 01ffce2cc..f193aeabd 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/SavePolicyCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/SavePolicyCommand.cs @@ -87,8 +87,7 @@ public class SavePolicyCommand : ISavePolicyCommand if (currentPolicy is not { Enabled: true } && policyUpdate.Enabled) { var missingRequiredPolicyTypes = validator.RequiredPolicies - .Where(requiredPolicyType => - savedPoliciesDict.GetValueOrDefault(requiredPolicyType) is not { Enabled: true }) + .Where(requiredPolicyType => savedPoliciesDict.GetValueOrDefault(requiredPolicyType) is not { Enabled: true }) .ToList(); if (missingRequiredPolicyTypes.Count != 0) diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs index 3e1f8d26c..cc6971f94 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs @@ -2,6 +2,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; using Bit.Core.Auth.Enums; @@ -23,7 +24,9 @@ public class SingleOrgPolicyValidator : IPolicyValidator private readonly IOrganizationRepository _organizationRepository; private readonly ISsoConfigRepository _ssoConfigRepository; private readonly ICurrentContext _currentContext; + private readonly IFeatureService _featureService; private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; + private readonly IOrganizationHasVerifiedDomainsQuery _organizationHasVerifiedDomainsQuery; public SingleOrgPolicyValidator( IOrganizationUserRepository organizationUserRepository, @@ -31,14 +34,18 @@ public class SingleOrgPolicyValidator : IPolicyValidator IOrganizationRepository organizationRepository, ISsoConfigRepository ssoConfigRepository, ICurrentContext currentContext, - IRemoveOrganizationUserCommand removeOrganizationUserCommand) + IFeatureService featureService, + IRemoveOrganizationUserCommand removeOrganizationUserCommand, + IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery) { _organizationUserRepository = organizationUserRepository; _mailService = mailService; _organizationRepository = organizationRepository; _ssoConfigRepository = ssoConfigRepository; _currentContext = currentContext; + _featureService = featureService; _removeOrganizationUserCommand = removeOrganizationUserCommand; + _organizationHasVerifiedDomainsQuery = organizationHasVerifiedDomainsQuery; } public IEnumerable RequiredPolicies => []; @@ -93,9 +100,21 @@ public class SingleOrgPolicyValidator : IPolicyValidator if (policyUpdate is not { Enabled: true }) { var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(policyUpdate.OrganizationId); - return ssoConfig.ValidateDecryptionOptionsNotEnabled([MemberDecryptionType.KeyConnector]); + + var validateDecryptionErrorMessage = ssoConfig.ValidateDecryptionOptionsNotEnabled([MemberDecryptionType.KeyConnector]); + + if (!string.IsNullOrWhiteSpace(validateDecryptionErrorMessage)) + { + return validateDecryptionErrorMessage; + } + + if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) + && await _organizationHasVerifiedDomainsQuery.HasVerifiedDomainsAsync(policyUpdate.OrganizationId)) + { + return "The Single organization policy is required for organizations that have enabled domain verification."; + } } - return ""; + return string.Empty; } } diff --git a/src/Core/AdminConsole/Services/Implementations/PolicyService.cs b/src/Core/AdminConsole/Services/Implementations/PolicyService.cs index 072aa8283..42655040a 100644 --- a/src/Core/AdminConsole/Services/Implementations/PolicyService.cs +++ b/src/Core/AdminConsole/Services/Implementations/PolicyService.cs @@ -289,7 +289,7 @@ public class PolicyService : IPolicyService if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) && await _organizationHasVerifiedDomainsQuery.HasVerifiedDomainsAsync(org.Id)) { - throw new BadRequestException("Organization has verified domains."); + throw new BadRequestException("The Single organization policy is required for organizations that have enabled domain verification."); } } diff --git a/test/Api.Test/AdminConsole/Models/Response/Helpers/PolicyDetailResponsesTests.cs b/test/Api.Test/AdminConsole/Models/Response/Helpers/PolicyDetailResponsesTests.cs new file mode 100644 index 000000000..c380185a7 --- /dev/null +++ b/test/Api.Test/AdminConsole/Models/Response/Helpers/PolicyDetailResponsesTests.cs @@ -0,0 +1,69 @@ +using AutoFixture; +using Bit.Api.AdminConsole.Models.Response.Helpers; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; +using NSubstitute; +using Xunit; + +namespace Bit.Api.Test.AdminConsole.Models.Response.Helpers; + +public class PolicyDetailResponsesTests +{ + [Fact] + public async Task GetSingleOrgPolicyDetailResponseAsync_GivenPolicyEntity_WhenIsSingleOrgTypeAndHasVerifiedDomains_ThenShouldNotBeAbleToToggle() + { + var fixture = new Fixture(); + + var policy = fixture.Build() + .Without(p => p.Data) + .With(p => p.Type, PolicyType.SingleOrg) + .Create(); + + var querySub = Substitute.For(); + querySub.HasVerifiedDomainsAsync(policy.OrganizationId) + .Returns(true); + + var result = await policy.GetSingleOrgPolicyDetailResponseAsync(querySub); + + Assert.False(result.CanToggleState); + } + + [Fact] + public async Task GetSingleOrgPolicyDetailResponseAsync_GivenPolicyEntity_WhenIsNotSingleOrgType_ThenShouldThrowArgumentException() + { + var fixture = new Fixture(); + + var policy = fixture.Build() + .Without(p => p.Data) + .With(p => p.Type, PolicyType.TwoFactorAuthentication) + .Create(); + + var querySub = Substitute.For(); + querySub.HasVerifiedDomainsAsync(policy.OrganizationId) + .Returns(true); + + var action = async () => await policy.GetSingleOrgPolicyDetailResponseAsync(querySub); + + await Assert.ThrowsAsync("policy", action); + } + + [Fact] + public async Task GetSingleOrgPolicyDetailResponseAsync_GivenPolicyEntity_WhenIsSingleOrgTypeAndDoesNotHaveVerifiedDomains_ThenShouldBeAbleToToggle() + { + var fixture = new Fixture(); + + var policy = fixture.Build() + .Without(p => p.Data) + .With(p => p.Type, PolicyType.SingleOrg) + .Create(); + + var querySub = Substitute.For(); + querySub.HasVerifiedDomainsAsync(policy.OrganizationId) + .Returns(false); + + var result = await policy.GetSingleOrgPolicyDetailResponseAsync(querySub); + + Assert.True(result.CanToggleState); + } +} diff --git a/test/Api.Test/Controllers/PoliciesControllerTests.cs b/test/Api.Test/Controllers/PoliciesControllerTests.cs index 77cc5ea02..1b96ace5d 100644 --- a/test/Api.Test/Controllers/PoliciesControllerTests.cs +++ b/test/Api.Test/Controllers/PoliciesControllerTests.cs @@ -1,9 +1,9 @@ using System.Security.Claims; using System.Text.Json; using Bit.Api.AdminConsole.Controllers; +using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; -using Bit.Core.AdminConsole.Models.Api.Response; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Context; @@ -157,7 +157,7 @@ public class PoliciesControllerTests var result = await sutProvider.Sut.Get(orgId, type); // Assert - Assert.IsType(result); + Assert.IsType(result); Assert.Equal(policy.Id, result.Id); Assert.Equal(policy.Type, result.Type); Assert.Equal(policy.Enabled, result.Enabled); @@ -182,7 +182,7 @@ public class PoliciesControllerTests var result = await sutProvider.Sut.Get(orgId, type); // Assert - Assert.IsType(result); + Assert.IsType(result); Assert.Equal(result.Type, (PolicyType)type); Assert.False(result.Enabled); } diff --git a/test/Core.Test/AdminConsole/Services/PolicyServiceTests.cs b/test/Core.Test/AdminConsole/Services/PolicyServiceTests.cs index da3f2b267..68f36e37c 100644 --- a/test/Core.Test/AdminConsole/Services/PolicyServiceTests.cs +++ b/test/Core.Test/AdminConsole/Services/PolicyServiceTests.cs @@ -842,6 +842,6 @@ public class PolicyServiceTests var badRequestException = await Assert.ThrowsAsync( () => sutProvider.Sut.SaveAsync(policy, null)); - Assert.Equal("Organization has verified domains.", badRequestException.Message); + Assert.Equal("The Single organization policy is required for organizations that have enabled domain verification.", badRequestException.Message); } } From 0e23a07bbcc70fe1a9756c6ff8830221c14570f7 Mon Sep 17 00:00:00 2001 From: Tom <144813356+ttalty@users.noreply.github.com> Date: Mon, 11 Nov 2024 11:18:10 -0500 Subject: [PATCH 19/29] [PM-13298] Modify members access logic (#4876) * Initial refactor of members acess * Refactor of the members access report to include a list of ciphers * Saving ciphers to parent object * Missed saving the response model * bit.core change and updating references. Removing unused refs * Removing commented code * Adding Bit to the namespaces * The mapping to the response model missed setting the UserId --- .../Tools/Controllers/ReportsController.cs | 97 ++++---- .../Response/MemberAccessReportModel.cs | 163 +------------- .../MemberCipherDetailsResponseModel.cs | 24 ++ .../Models/Data/MemberAccessCipherDetails.cs | 43 ++++ .../MemberAccessCipherDetailsQuery.cs | 208 ++++++++++++++++++ .../IMemberAccessCipherDetailsQuery.cs | 9 + .../ReportingServiceCollectionExtensions.cs | 13 ++ .../MemberAccessCipherDetailsRequest.cs | 6 + .../Utilities/ServiceCollectionExtensions.cs | 2 + 9 files changed, 370 insertions(+), 195 deletions(-) create mode 100644 src/Api/Tools/Models/Response/MemberCipherDetailsResponseModel.cs create mode 100644 src/Core/Tools/Models/Data/MemberAccessCipherDetails.cs create mode 100644 src/Core/Tools/ReportFeatures/MemberAccessCipherDetailsQuery.cs create mode 100644 src/Core/Tools/ReportFeatures/OrganizationReportMembers/Interfaces/IMemberAccessCipherDetailsQuery.cs create mode 100644 src/Core/Tools/ReportFeatures/ReportingServiceCollectionExtensions.cs create mode 100644 src/Core/Tools/ReportFeatures/Requests/MemberAccessCipherDetailsRequest.cs diff --git a/src/Api/Tools/Controllers/ReportsController.cs b/src/Api/Tools/Controllers/ReportsController.cs index 5beb320e4..c8cfc0a21 100644 --- a/src/Api/Tools/Controllers/ReportsController.cs +++ b/src/Api/Tools/Controllers/ReportsController.cs @@ -1,13 +1,9 @@ using Bit.Api.Tools.Models.Response; -using Bit.Core.AdminConsole.Repositories; -using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Context; using Bit.Core.Exceptions; -using Bit.Core.Repositories; -using Bit.Core.Services; -using Bit.Core.Vault.Queries; -using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; -using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests; +using Bit.Core.Tools.Models.Data; +using Bit.Core.Tools.ReportFeatures.OrganizationReportMembers.Interfaces; +using Bit.Core.Tools.ReportFeatures.Requests; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -17,33 +13,49 @@ namespace Bit.Api.Tools.Controllers; [Authorize("Application")] public class ReportsController : Controller { - private readonly IOrganizationUserUserDetailsQuery _organizationUserUserDetailsQuery; - private readonly IGroupRepository _groupRepository; - private readonly ICollectionRepository _collectionRepository; private readonly ICurrentContext _currentContext; - private readonly IOrganizationCiphersQuery _organizationCiphersQuery; - private readonly IApplicationCacheService _applicationCacheService; - private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; + private readonly IMemberAccessCipherDetailsQuery _memberAccessCipherDetailsQuery; public ReportsController( - IOrganizationUserUserDetailsQuery organizationUserUserDetailsQuery, - IGroupRepository groupRepository, - ICollectionRepository collectionRepository, ICurrentContext currentContext, - IOrganizationCiphersQuery organizationCiphersQuery, - IApplicationCacheService applicationCacheService, - ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery + IMemberAccessCipherDetailsQuery memberAccessCipherDetailsQuery ) { - _organizationUserUserDetailsQuery = organizationUserUserDetailsQuery; - _groupRepository = groupRepository; - _collectionRepository = collectionRepository; _currentContext = currentContext; - _organizationCiphersQuery = organizationCiphersQuery; - _applicationCacheService = applicationCacheService; - _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; + _memberAccessCipherDetailsQuery = memberAccessCipherDetailsQuery; } + /// + /// Organization member information containing a list of cipher ids + /// assigned + /// + /// Organzation Id + /// IEnumerable of MemberCipherDetailsResponseModel + /// If Access reports permission is not assigned + [HttpGet("member-cipher-details/{orgId}")] + public async Task> GetMemberCipherDetails(Guid orgId) + { + // Using the AccessReports permission here until new permissions + // are needed for more control over reports + if (!await _currentContext.AccessReports(orgId)) + { + throw new NotFoundException(); + } + + var memberCipherDetails = await GetMemberCipherDetails(new MemberAccessCipherDetailsRequest { OrganizationId = orgId }); + + var responses = memberCipherDetails.Select(x => new MemberCipherDetailsResponseModel(x)); + + return responses; + } + + /// + /// Access details for an organization member. Includes the member information, + /// group collection assignment, and item counts + /// + /// Organization Id + /// IEnumerable of MemberAccessReportResponseModel + /// If Access reports permission is not assigned [HttpGet("member-access/{orgId}")] public async Task> GetMemberAccessReport(Guid orgId) { @@ -52,26 +64,23 @@ public class ReportsController : Controller throw new NotFoundException(); } - var orgUsers = await _organizationUserUserDetailsQuery.GetOrganizationUserUserDetails( - new OrganizationUserUserDetailsQueryRequest - { - OrganizationId = orgId, - IncludeCollections = true, - IncludeGroups = true - }); + var memberCipherDetails = await GetMemberCipherDetails(new MemberAccessCipherDetailsRequest { OrganizationId = orgId }); - var orgGroups = await _groupRepository.GetManyByOrganizationIdAsync(orgId); - var orgAbility = await _applicationCacheService.GetOrganizationAbilityAsync(orgId); - var orgCollectionsWithAccess = await _collectionRepository.GetManyByOrganizationIdWithAccessAsync(orgId); - var orgItems = await _organizationCiphersQuery.GetAllOrganizationCiphers(orgId); - var organizationUsersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(orgUsers); + var responses = memberCipherDetails.Select(x => new MemberAccessReportResponseModel(x)); - var reports = MemberAccessReportResponseModel.CreateReport( - orgGroups, - orgCollectionsWithAccess, - orgItems, - organizationUsersTwoFactorEnabled, - orgAbility); - return reports; + return responses; + } + + /// + /// Contains the organization member info, the cipher ids associated with the member, + /// and details on their collections, groups, and permissions + /// + /// Request to the MemberAccessCipherDetailsQuery + /// IEnumerable of MemberAccessCipherDetails + private async Task> GetMemberCipherDetails(MemberAccessCipherDetailsRequest request) + { + var memberCipherDetails = + await _memberAccessCipherDetailsQuery.GetMemberAccessCipherDetails(request); + return memberCipherDetails; } } diff --git a/src/Api/Tools/Models/Response/MemberAccessReportModel.cs b/src/Api/Tools/Models/Response/MemberAccessReportModel.cs index 378d4d94c..b110c316c 100644 --- a/src/Api/Tools/Models/Response/MemberAccessReportModel.cs +++ b/src/Api/Tools/Models/Response/MemberAccessReportModel.cs @@ -1,30 +1,7 @@ -using Bit.Core.AdminConsole.Entities; -using Bit.Core.Entities; -using Bit.Core.Models.Data; -using Bit.Core.Models.Data.Organizations; -using Bit.Core.Models.Data.Organizations.OrganizationUsers; -using Bit.Core.Vault.Models.Data; +using Bit.Core.Tools.Models.Data; namespace Bit.Api.Tools.Models.Response; -/// -/// Member access details. The individual item for the detailed member access -/// report. A collection can be assigned directly to a user without a group or -/// the user can be assigned to a collection through a group. Group level permissions -/// can override collection level permissions. -/// -public class MemberAccessReportAccessDetails -{ - public Guid? CollectionId { get; set; } - public Guid? GroupId { get; set; } - public string GroupName { get; set; } - public string CollectionName { get; set; } - public int ItemCount { get; set; } - public bool? ReadOnly { get; set; } - public bool? HidePasswords { get; set; } - public bool? Manage { get; set; } -} - /// /// Contains the collections and group collections a user has access to including /// the permission level for the collection and group collection. @@ -40,134 +17,18 @@ public class MemberAccessReportResponseModel public int TotalItemCount { get; set; } public Guid? UserGuid { get; set; } public bool UsesKeyConnector { get; set; } - public IEnumerable AccessDetails { get; set; } + public IEnumerable AccessDetails { get; set; } - /// - /// Generates a report for all members of an organization. Containing summary information - /// such as item, collection, and group counts. As well as detailed information on the - /// user and group collections along with their permissions - /// - /// Organization groups collection - /// Collections for the organization and the groups/users and permissions - /// Cipher items for the organization with the collections associated with them - /// Organization users and two factor status - /// Organization ability for account recovery status - /// List of the MemberAccessReportResponseModel; - public static IEnumerable CreateReport( - ICollection orgGroups, - ICollection> orgCollectionsWithAccess, - IEnumerable orgItems, - IEnumerable<(OrganizationUserUserDetails user, bool twoFactorIsEnabled)> organizationUsersTwoFactorEnabled, - OrganizationAbility orgAbility) + public MemberAccessReportResponseModel(MemberAccessCipherDetails memberAccessCipherDetails) { - var orgUsers = organizationUsersTwoFactorEnabled.Select(x => x.user); - // Create a dictionary to lookup the group names later. - var groupNameDictionary = orgGroups.ToDictionary(x => x.Id, x => x.Name); - - // Get collections grouped and into a dictionary for counts - var collectionItems = orgItems - .SelectMany(x => x.CollectionIds, - (x, b) => new { CipherId = x.Id, CollectionId = b }) - .GroupBy(y => y.CollectionId, - (key, g) => new { CollectionId = key, Ciphers = g }); - var collectionItemCounts = collectionItems.ToDictionary(x => x.CollectionId, x => x.Ciphers.Count()); - - - // Loop through the org users and populate report and access data - var memberAccessReport = new List(); - foreach (var user in orgUsers) - { - // Take the collections/groups and create the access details items - var groupAccessDetails = new List(); - var userCollectionAccessDetails = new List(); - foreach (var tCollect in orgCollectionsWithAccess) - { - var itemCounts = collectionItemCounts.TryGetValue(tCollect.Item1.Id, out var itemCount) ? itemCount : 0; - if (tCollect.Item2.Groups.Count() > 0) - { - var groupDetails = tCollect.Item2.Groups.Where((tCollectGroups) => user.Groups.Contains(tCollectGroups.Id)).Select(x => - new MemberAccessReportAccessDetails - { - CollectionId = tCollect.Item1.Id, - CollectionName = tCollect.Item1.Name, - GroupId = x.Id, - GroupName = groupNameDictionary[x.Id], - ReadOnly = x.ReadOnly, - HidePasswords = x.HidePasswords, - Manage = x.Manage, - ItemCount = itemCounts, - }); - groupAccessDetails.AddRange(groupDetails); - } - - // All collections assigned to users and their permissions - if (tCollect.Item2.Users.Count() > 0) - { - var userCollectionDetails = tCollect.Item2.Users.Where((tCollectUser) => tCollectUser.Id == user.Id).Select(x => - new MemberAccessReportAccessDetails - { - CollectionId = tCollect.Item1.Id, - CollectionName = tCollect.Item1.Name, - ReadOnly = x.ReadOnly, - HidePasswords = x.HidePasswords, - Manage = x.Manage, - ItemCount = itemCounts, - }); - userCollectionAccessDetails.AddRange(userCollectionDetails); - } - } - - var report = new MemberAccessReportResponseModel - { - UserName = user.Name, - Email = user.Email, - TwoFactorEnabled = organizationUsersTwoFactorEnabled.FirstOrDefault(u => u.user.Id == user.Id).twoFactorIsEnabled, - // Both the user's ResetPasswordKey must be set and the organization can UseResetPassword - AccountRecoveryEnabled = !string.IsNullOrEmpty(user.ResetPasswordKey) && orgAbility.UseResetPassword, - UserGuid = user.Id, - UsesKeyConnector = user.UsesKeyConnector - }; - - var userAccessDetails = new List(); - if (user.Groups.Any()) - { - var userGroups = groupAccessDetails.Where(x => user.Groups.Contains(x.GroupId.GetValueOrDefault())); - userAccessDetails.AddRange(userGroups); - } - - // There can be edge cases where groups don't have a collection - var groupsWithoutCollections = user.Groups.Where(x => !userAccessDetails.Any(y => x == y.GroupId)); - if (groupsWithoutCollections.Count() > 0) - { - var emptyGroups = groupsWithoutCollections.Select(x => new MemberAccessReportAccessDetails - { - GroupId = x, - GroupName = groupNameDictionary[x], - ItemCount = 0 - }); - userAccessDetails.AddRange(emptyGroups); - } - - if (user.Collections.Any()) - { - var userCollections = userCollectionAccessDetails.Where(x => user.Collections.Any(y => x.CollectionId == y.Id)); - userAccessDetails.AddRange(userCollections); - } - report.AccessDetails = userAccessDetails; - - report.TotalItemCount = collectionItems - .Where(x => report.AccessDetails.Any(y => x.CollectionId == y.CollectionId)) - .SelectMany(x => x.Ciphers) - .GroupBy(g => g.CipherId).Select(grp => grp.FirstOrDefault()) - .Count(); - - // Distinct items only - var distinctItems = report.AccessDetails.Where(x => x.CollectionId.HasValue).Select(x => x.CollectionId).Distinct(); - report.CollectionsCount = distinctItems.Count(); - report.GroupsCount = report.AccessDetails.Select(x => x.GroupId).Where(y => y.HasValue).Distinct().Count(); - memberAccessReport.Add(report); - } - return memberAccessReport; + this.UserName = memberAccessCipherDetails.UserName; + this.Email = memberAccessCipherDetails.Email; + this.TwoFactorEnabled = memberAccessCipherDetails.TwoFactorEnabled; + this.AccountRecoveryEnabled = memberAccessCipherDetails.AccountRecoveryEnabled; + this.GroupsCount = memberAccessCipherDetails.GroupsCount; + this.CollectionsCount = memberAccessCipherDetails.CollectionsCount; + this.TotalItemCount = memberAccessCipherDetails.TotalItemCount; + this.UserGuid = memberAccessCipherDetails.UserGuid; + this.AccessDetails = memberAccessCipherDetails.AccessDetails; } - } diff --git a/src/Api/Tools/Models/Response/MemberCipherDetailsResponseModel.cs b/src/Api/Tools/Models/Response/MemberCipherDetailsResponseModel.cs new file mode 100644 index 000000000..5c87264c5 --- /dev/null +++ b/src/Api/Tools/Models/Response/MemberCipherDetailsResponseModel.cs @@ -0,0 +1,24 @@ +using Bit.Core.Tools.Models.Data; + +namespace Bit.Api.Tools.Models.Response; + +public class MemberCipherDetailsResponseModel +{ + public string UserName { get; set; } + public string Email { get; set; } + public bool UsesKeyConnector { get; set; } + + /// + /// A distinct list of the cipher ids associated with + /// the organization member + /// + public IEnumerable CipherIds { get; set; } + + public MemberCipherDetailsResponseModel(MemberAccessCipherDetails memberAccessCipherDetails) + { + this.UserName = memberAccessCipherDetails.UserName; + this.Email = memberAccessCipherDetails.Email; + this.UsesKeyConnector = memberAccessCipherDetails.UsesKeyConnector; + this.CipherIds = memberAccessCipherDetails.CipherIds; + } +} diff --git a/src/Core/Tools/Models/Data/MemberAccessCipherDetails.cs b/src/Core/Tools/Models/Data/MemberAccessCipherDetails.cs new file mode 100644 index 000000000..943d56c53 --- /dev/null +++ b/src/Core/Tools/Models/Data/MemberAccessCipherDetails.cs @@ -0,0 +1,43 @@ +namespace Bit.Core.Tools.Models.Data; + +public class MemberAccessDetails +{ + public Guid? CollectionId { get; set; } + public Guid? GroupId { get; set; } + public string GroupName { get; set; } + public string CollectionName { get; set; } + public int ItemCount { get; set; } + public bool? ReadOnly { get; set; } + public bool? HidePasswords { get; set; } + public bool? Manage { get; set; } + + /// + /// The CipherIds associated with the group/collection access + /// + public IEnumerable CollectionCipherIds { get; set; } +} + +public class MemberAccessCipherDetails +{ + public string UserName { get; set; } + public string Email { get; set; } + public bool TwoFactorEnabled { get; set; } + public bool AccountRecoveryEnabled { get; set; } + public int GroupsCount { get; set; } + public int CollectionsCount { get; set; } + public int TotalItemCount { get; set; } + public Guid? UserGuid { get; set; } + public bool UsesKeyConnector { get; set; } + + /// + /// The details for the member's collection access depending + /// on the collections and groups they are assigned to + /// + public IEnumerable AccessDetails { get; set; } + + /// + /// A distinct list of the cipher ids associated with + /// the organization member + /// + public IEnumerable CipherIds { get; set; } +} diff --git a/src/Core/Tools/ReportFeatures/MemberAccessCipherDetailsQuery.cs b/src/Core/Tools/ReportFeatures/MemberAccessCipherDetailsQuery.cs new file mode 100644 index 000000000..a08359a84 --- /dev/null +++ b/src/Core/Tools/ReportFeatures/MemberAccessCipherDetailsQuery.cs @@ -0,0 +1,208 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; +using Bit.Core.Entities; +using Bit.Core.Models.Data; +using Bit.Core.Models.Data.Organizations; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Tools.Models.Data; +using Bit.Core.Tools.ReportFeatures.OrganizationReportMembers.Interfaces; +using Bit.Core.Tools.ReportFeatures.Requests; +using Bit.Core.Vault.Models.Data; +using Bit.Core.Vault.Queries; +using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; +using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests; + +namespace Bit.Core.Tools.ReportFeatures; + +public class MemberAccessCipherDetailsQuery : IMemberAccessCipherDetailsQuery +{ + private readonly IOrganizationUserUserDetailsQuery _organizationUserUserDetailsQuery; + private readonly IGroupRepository _groupRepository; + private readonly ICollectionRepository _collectionRepository; + private readonly IOrganizationCiphersQuery _organizationCiphersQuery; + private readonly IApplicationCacheService _applicationCacheService; + private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; + + public MemberAccessCipherDetailsQuery( + IOrganizationUserUserDetailsQuery organizationUserUserDetailsQuery, + IGroupRepository groupRepository, + ICollectionRepository collectionRepository, + IOrganizationCiphersQuery organizationCiphersQuery, + IApplicationCacheService applicationCacheService, + ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery + ) + { + _organizationUserUserDetailsQuery = organizationUserUserDetailsQuery; + _groupRepository = groupRepository; + _collectionRepository = collectionRepository; + _organizationCiphersQuery = organizationCiphersQuery; + _applicationCacheService = applicationCacheService; + _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; + } + + public async Task> GetMemberAccessCipherDetails(MemberAccessCipherDetailsRequest request) + { + var orgUsers = await _organizationUserUserDetailsQuery.GetOrganizationUserUserDetails( + new OrganizationUserUserDetailsQueryRequest + { + OrganizationId = request.OrganizationId, + IncludeCollections = true, + IncludeGroups = true + }); + + var orgGroups = await _groupRepository.GetManyByOrganizationIdAsync(request.OrganizationId); + var orgAbility = await _applicationCacheService.GetOrganizationAbilityAsync(request.OrganizationId); + var orgCollectionsWithAccess = await _collectionRepository.GetManyByOrganizationIdWithAccessAsync(request.OrganizationId); + var orgItems = await _organizationCiphersQuery.GetAllOrganizationCiphers(request.OrganizationId); + var organizationUsersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(orgUsers); + + var memberAccessCipherDetails = GenerateAccessData( + orgGroups, + orgCollectionsWithAccess, + orgItems, + organizationUsersTwoFactorEnabled, + orgAbility + ); + + return memberAccessCipherDetails; + } + + /// + /// Generates a report for all members of an organization. Containing summary information + /// such as item, collection, and group counts. Including the cipherIds a member is assigned. + /// Child collection includes detailed information on the user and group collections along + /// with their permissions. + /// + /// Organization groups collection + /// Collections for the organization and the groups/users and permissions + /// Cipher items for the organization with the collections associated with them + /// Organization users and two factor status + /// Organization ability for account recovery status + /// List of the MemberAccessCipherDetailsModel; + private IEnumerable GenerateAccessData( + ICollection orgGroups, + ICollection> orgCollectionsWithAccess, + IEnumerable orgItems, + IEnumerable<(OrganizationUserUserDetails user, bool twoFactorIsEnabled)> organizationUsersTwoFactorEnabled, + OrganizationAbility orgAbility) + { + var orgUsers = organizationUsersTwoFactorEnabled.Select(x => x.user); + // Create a dictionary to lookup the group names later. + var groupNameDictionary = orgGroups.ToDictionary(x => x.Id, x => x.Name); + + // Get collections grouped and into a dictionary for counts + var collectionItems = orgItems + .SelectMany(x => x.CollectionIds, + (cipher, collectionId) => new { Cipher = cipher, CollectionId = collectionId }) + .GroupBy(y => y.CollectionId, + (key, ciphers) => new { CollectionId = key, Ciphers = ciphers }); + var itemLookup = collectionItems.ToDictionary(x => x.CollectionId.ToString(), x => x.Ciphers.Select(c => c.Cipher.Id.ToString())); + + // Loop through the org users and populate report and access data + var memberAccessCipherDetails = new List(); + foreach (var user in orgUsers) + { + var groupAccessDetails = new List(); + var userCollectionAccessDetails = new List(); + foreach (var tCollect in orgCollectionsWithAccess) + { + var hasItems = itemLookup.TryGetValue(tCollect.Item1.Id.ToString(), out var items); + var collectionCiphers = hasItems ? items.Select(x => x) : null; + + var itemCounts = hasItems ? collectionCiphers.Count() : 0; + if (tCollect.Item2.Groups.Count() > 0) + { + + var groupDetails = tCollect.Item2.Groups.Where((tCollectGroups) => user.Groups.Contains(tCollectGroups.Id)).Select(x => + new MemberAccessDetails + { + CollectionId = tCollect.Item1.Id, + CollectionName = tCollect.Item1.Name, + GroupId = x.Id, + GroupName = groupNameDictionary[x.Id], + ReadOnly = x.ReadOnly, + HidePasswords = x.HidePasswords, + Manage = x.Manage, + ItemCount = itemCounts, + CollectionCipherIds = items + }); + + groupAccessDetails.AddRange(groupDetails); + } + + // All collections assigned to users and their permissions + if (tCollect.Item2.Users.Count() > 0) + { + var userCollectionDetails = tCollect.Item2.Users.Where((tCollectUser) => tCollectUser.Id == user.Id).Select(x => + new MemberAccessDetails + { + CollectionId = tCollect.Item1.Id, + CollectionName = tCollect.Item1.Name, + ReadOnly = x.ReadOnly, + HidePasswords = x.HidePasswords, + Manage = x.Manage, + ItemCount = itemCounts, + CollectionCipherIds = items + }); + userCollectionAccessDetails.AddRange(userCollectionDetails); + } + } + + var report = new MemberAccessCipherDetails + { + UserName = user.Name, + Email = user.Email, + TwoFactorEnabled = organizationUsersTwoFactorEnabled.FirstOrDefault(u => u.user.Id == user.Id).twoFactorIsEnabled, + // Both the user's ResetPasswordKey must be set and the organization can UseResetPassword + AccountRecoveryEnabled = !string.IsNullOrEmpty(user.ResetPasswordKey) && orgAbility.UseResetPassword, + UserGuid = user.Id, + UsesKeyConnector = user.UsesKeyConnector + }; + + var userAccessDetails = new List(); + if (user.Groups.Any()) + { + var userGroups = groupAccessDetails.Where(x => user.Groups.Contains(x.GroupId.GetValueOrDefault())); + userAccessDetails.AddRange(userGroups); + } + + // There can be edge cases where groups don't have a collection + var groupsWithoutCollections = user.Groups.Where(x => !userAccessDetails.Any(y => x == y.GroupId)); + if (groupsWithoutCollections.Count() > 0) + { + var emptyGroups = groupsWithoutCollections.Select(x => new MemberAccessDetails + { + GroupId = x, + GroupName = groupNameDictionary[x], + ItemCount = 0 + }); + userAccessDetails.AddRange(emptyGroups); + } + + if (user.Collections.Any()) + { + var userCollections = userCollectionAccessDetails.Where(x => user.Collections.Any(y => x.CollectionId == y.Id)); + userAccessDetails.AddRange(userCollections); + } + report.AccessDetails = userAccessDetails; + + var userCiphers = + report.AccessDetails + .Where(x => x.ItemCount > 0) + .SelectMany(y => y.CollectionCipherIds) + .Distinct(); + report.CipherIds = userCiphers; + report.TotalItemCount = userCiphers.Count(); + + // Distinct items only + var distinctItems = report.AccessDetails.Where(x => x.CollectionId.HasValue).Select(x => x.CollectionId).Distinct(); + report.CollectionsCount = distinctItems.Count(); + report.GroupsCount = report.AccessDetails.Select(x => x.GroupId).Where(y => y.HasValue).Distinct().Count(); + memberAccessCipherDetails.Add(report); + } + return memberAccessCipherDetails; + } +} diff --git a/src/Core/Tools/ReportFeatures/OrganizationReportMembers/Interfaces/IMemberAccessCipherDetailsQuery.cs b/src/Core/Tools/ReportFeatures/OrganizationReportMembers/Interfaces/IMemberAccessCipherDetailsQuery.cs new file mode 100644 index 000000000..c55495fd1 --- /dev/null +++ b/src/Core/Tools/ReportFeatures/OrganizationReportMembers/Interfaces/IMemberAccessCipherDetailsQuery.cs @@ -0,0 +1,9 @@ +using Bit.Core.Tools.Models.Data; +using Bit.Core.Tools.ReportFeatures.Requests; + +namespace Bit.Core.Tools.ReportFeatures.OrganizationReportMembers.Interfaces; + +public interface IMemberAccessCipherDetailsQuery +{ + Task> GetMemberAccessCipherDetails(MemberAccessCipherDetailsRequest request); +} diff --git a/src/Core/Tools/ReportFeatures/ReportingServiceCollectionExtensions.cs b/src/Core/Tools/ReportFeatures/ReportingServiceCollectionExtensions.cs new file mode 100644 index 000000000..5c813b8cb --- /dev/null +++ b/src/Core/Tools/ReportFeatures/ReportingServiceCollectionExtensions.cs @@ -0,0 +1,13 @@ + +using Bit.Core.Tools.ReportFeatures.OrganizationReportMembers.Interfaces; +using Microsoft.Extensions.DependencyInjection; + +namespace Bit.Core.Tools.ReportFeatures; + +public static class ReportingServiceCollectionExtensions +{ + public static void AddReportingServices(this IServiceCollection services) + { + services.AddScoped(); + } +} diff --git a/src/Core/Tools/ReportFeatures/Requests/MemberAccessCipherDetailsRequest.cs b/src/Core/Tools/ReportFeatures/Requests/MemberAccessCipherDetailsRequest.cs new file mode 100644 index 000000000..395230f43 --- /dev/null +++ b/src/Core/Tools/ReportFeatures/Requests/MemberAccessCipherDetailsRequest.cs @@ -0,0 +1,6 @@ +namespace Bit.Core.Tools.ReportFeatures.Requests; + +public class MemberAccessCipherDetailsRequest +{ + public Guid OrganizationId { get; set; } +} diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index 5a5585952..1b99b4cc8 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -34,6 +34,7 @@ using Bit.Core.SecretsManager.Repositories.Noop; using Bit.Core.Services; using Bit.Core.Settings; using Bit.Core.Tokens; +using Bit.Core.Tools.ReportFeatures; using Bit.Core.Tools.Services; using Bit.Core.Utilities; using Bit.Core.Vault; @@ -116,6 +117,7 @@ public static class ServiceCollectionExtensions services.AddLoginServices(); services.AddScoped(); services.AddVaultServices(); + services.AddReportingServices(); } public static void AddTokenizers(this IServiceCollection services) From 9fb3f1d34658b9bae6d5bb798d6c93408daf2030 Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Mon, 11 Nov 2024 11:54:52 -0600 Subject: [PATCH 20/29] PM-13237 password health report application add get (#5000) * PM-13236 PasswordHealthReportApplications db * PM-13236 incorporated pr comments * PM-13236 fixed error in SQL script * PM-13236 resolve quality scan errors SQL71006, SQL7101, SQL70001 * PM-13236 fixed warnings on procedures * PM-13236 added efMigrations * PM-13236 renamed files to PasswordHealthReportApplication (singular) * PM-13236 changed file name to more appropriate naming * PM-13236 changed the file name singular * PM-13236 PasswordHealthReportApplication Entities and Repos * PM-13236 moved files under tools from core * PM-13236 Entity PasswordHealthReportApplication namespace changed to tools/entities * PM-13236 moved Repos and Interfaces to tools * PM-13236 migrated model to tools namespace * PM-13236 minor fixes to the unit tests * PM-13236 fixed script errors during build * PM-13236 Script to drop PasswordHealthReportApplications if it exists * PM-13236 fixes to database snapshot * PM-13236 updated databasesnapshots * PM-13236 Update database model changes for Mysql * PM-13236 update model changes for Sqlite * PM-13236 updated the models to remove commented code * PM-13236 added correct db snapshot for MySql * PM-13236 updated database snapshot for Postgres * PM-13236 updated database snapshot for Sqlite * PM-13236 removed unwanted directive to fix linting error * PM-13236 removed redundant script files * PM-13237 Add entity command and unit tests * PM-13237 Get query added with unit tests * PM-13237 Controller to add/get PasswordHealthReportApplication * PM-13237 Setup dependencies in the EF Service collection extensions * PM-13237 Added unit tests for ReportsController --- src/Api/Startup.cs | 3 + .../Tools/Controllers/ReportsController.cs | 82 +++++++++- .../PasswordHealthReportApplicationModel.cs | 7 + ...dPasswordHealthReportApplicationCommand.cs | 101 ++++++++++++ ...GetPasswordHealthReportApplicationQuery.cs | 27 ++++ ...dPasswordHealthReportApplicationCommand.cs | 10 ++ ...GetPasswordHealthReportApplicationQuery.cs | 8 + .../ReportingServiceCollectionExtensions.cs | 4 +- ...dPasswordHealthReportApplicationRequest.cs | 7 + ...ityFrameworkServiceCollectionExtensions.cs | 1 + .../Controllers/ReportsControllerTests.cs | 49 ++++++ ...wordHealthReportApplicationCommandTests.cs | 149 ++++++++++++++++++ ...sswordHealthReportApplicationQueryTests.cs | 53 +++++++ 13 files changed, 498 insertions(+), 3 deletions(-) create mode 100644 src/Api/Tools/Models/PasswordHealthReportApplicationModel.cs create mode 100644 src/Core/Tools/ReportFeatures/AddPasswordHealthReportApplicationCommand.cs create mode 100644 src/Core/Tools/ReportFeatures/GetPasswordHealthReportApplicationQuery.cs create mode 100644 src/Core/Tools/ReportFeatures/Interfaces/IAddPasswordHealthReportApplicationCommand.cs create mode 100644 src/Core/Tools/ReportFeatures/Interfaces/IGetPasswordHealthReportApplicationQuery.cs create mode 100644 src/Core/Tools/Requests/AddPasswordHealthReportApplicationRequest.cs create mode 100644 test/Api.Test/Tools/Controllers/ReportsControllerTests.cs create mode 100644 test/Core.Test/Tools/ReportFeatures/AddPasswordHealthReportApplicationCommandTests.cs create mode 100644 test/Core.Test/Tools/ReportFeatures/GetPasswordHealthReportApplicationQueryTests.cs diff --git a/src/Api/Startup.cs b/src/Api/Startup.cs index 8a7721bcb..7962215a4 100644 --- a/src/Api/Startup.cs +++ b/src/Api/Startup.cs @@ -32,6 +32,8 @@ using Bit.Core.Tools.Entities; using Bit.Core.Vault.Entities; using Bit.Api.Auth.Models.Request.WebAuthn; using Bit.Core.Auth.Models.Data; +using Bit.Core.Tools.ReportFeatures; + #if !OSS @@ -176,6 +178,7 @@ public class Startup services.AddOrganizationSubscriptionServices(); services.AddCoreLocalizationServices(); services.AddBillingOperations(); + services.AddReportingServices(); // Authorization Handlers services.AddAuthorizationHandlers(); diff --git a/src/Api/Tools/Controllers/ReportsController.cs b/src/Api/Tools/Controllers/ReportsController.cs index c8cfc0a21..9f465c7b8 100644 --- a/src/Api/Tools/Controllers/ReportsController.cs +++ b/src/Api/Tools/Controllers/ReportsController.cs @@ -1,9 +1,13 @@ -using Bit.Api.Tools.Models.Response; +using Bit.Api.Tools.Models; +using Bit.Api.Tools.Models.Response; using Bit.Core.Context; using Bit.Core.Exceptions; +using Bit.Core.Tools.Entities; using Bit.Core.Tools.Models.Data; +using Bit.Core.Tools.ReportFeatures.Interfaces; using Bit.Core.Tools.ReportFeatures.OrganizationReportMembers.Interfaces; using Bit.Core.Tools.ReportFeatures.Requests; +using Bit.Core.Tools.Requests; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -15,14 +19,20 @@ public class ReportsController : Controller { private readonly ICurrentContext _currentContext; private readonly IMemberAccessCipherDetailsQuery _memberAccessCipherDetailsQuery; + private readonly IAddPasswordHealthReportApplicationCommand _addPwdHealthReportAppCommand; + private readonly IGetPasswordHealthReportApplicationQuery _getPwdHealthReportAppQuery; public ReportsController( ICurrentContext currentContext, - IMemberAccessCipherDetailsQuery memberAccessCipherDetailsQuery + IMemberAccessCipherDetailsQuery memberAccessCipherDetailsQuery, + IAddPasswordHealthReportApplicationCommand addPasswordHealthReportApplicationCommand, + IGetPasswordHealthReportApplicationQuery getPasswordHealthReportApplicationQuery ) { _currentContext = currentContext; _memberAccessCipherDetailsQuery = memberAccessCipherDetailsQuery; + _addPwdHealthReportAppCommand = addPasswordHealthReportApplicationCommand; + _getPwdHealthReportAppQuery = getPasswordHealthReportApplicationQuery; } /// @@ -83,4 +93,72 @@ public class ReportsController : Controller await _memberAccessCipherDetailsQuery.GetMemberAccessCipherDetails(request); return memberCipherDetails; } + + /// + /// Get the password health report applications for an organization + /// + /// A valid Organization Id + /// An Enumerable of PasswordHealthReportApplication + /// If the user lacks access + /// If the organization Id is not valid + [HttpGet("password-health-report-applications/{orgId}")] + public async Task> GetPasswordHealthReportApplications(Guid orgId) + { + if (!await _currentContext.AccessReports(orgId)) + { + throw new NotFoundException(); + } + + return await _getPwdHealthReportAppQuery.GetPasswordHealthReportApplicationAsync(orgId); + } + + /// + /// Adds a new record into PasswordHealthReportApplication + /// + /// A single instance of PasswordHealthReportApplication Model + /// A single instance of PasswordHealthReportApplication + /// If the organization Id is not valid + /// If the user lacks access + [HttpPost("password-health-report-application")] + public async Task AddPasswordHealthReportApplication( + [FromBody] PasswordHealthReportApplicationModel request) + { + if (!await _currentContext.AccessReports(request.OrganizationId)) + { + throw new NotFoundException(); + } + + var commandRequest = new AddPasswordHealthReportApplicationRequest + { + OrganizationId = request.OrganizationId, + Url = request.Url + }; + + return await _addPwdHealthReportAppCommand.AddPasswordHealthReportApplicationAsync(commandRequest); + } + + /// + /// Adds multiple records into PasswordHealthReportApplication + /// + /// A enumerable of PasswordHealthReportApplicationModel + /// An Enumerable of PasswordHealthReportApplication + /// If user does not have access to the OrganizationId + /// If the organization Id is not valid + [HttpPost("password-health-report-applications")] + public async Task> AddPasswordHealthReportApplications( + [FromBody] IEnumerable request) + { + if (request.Any(_ => _currentContext.AccessReports(_.OrganizationId).Result == false)) + { + throw new NotFoundException(); + } + + var commandRequests = request.Select(request => new AddPasswordHealthReportApplicationRequest + { + OrganizationId = request.OrganizationId, + Url = request.Url + }).ToList(); + + return await _addPwdHealthReportAppCommand.AddPasswordHealthReportApplicationAsync(commandRequests); + } } diff --git a/src/Api/Tools/Models/PasswordHealthReportApplicationModel.cs b/src/Api/Tools/Models/PasswordHealthReportApplicationModel.cs new file mode 100644 index 000000000..93467e117 --- /dev/null +++ b/src/Api/Tools/Models/PasswordHealthReportApplicationModel.cs @@ -0,0 +1,7 @@ +namespace Bit.Api.Tools.Models; + +public class PasswordHealthReportApplicationModel +{ + public Guid OrganizationId { get; set; } + public string Url { get; set; } +} diff --git a/src/Core/Tools/ReportFeatures/AddPasswordHealthReportApplicationCommand.cs b/src/Core/Tools/ReportFeatures/AddPasswordHealthReportApplicationCommand.cs new file mode 100644 index 000000000..c6bdb4417 --- /dev/null +++ b/src/Core/Tools/ReportFeatures/AddPasswordHealthReportApplicationCommand.cs @@ -0,0 +1,101 @@ +using Bit.Core.Exceptions; +using Bit.Core.Repositories; +using Bit.Core.Tools.Entities; +using Bit.Core.Tools.ReportFeatures.Interfaces; +using Bit.Core.Tools.Repositories; +using Bit.Core.Tools.Requests; + +namespace Bit.Core.Tools.ReportFeatures; + +public class AddPasswordHealthReportApplicationCommand : IAddPasswordHealthReportApplicationCommand +{ + private IOrganizationRepository _organizationRepo; + private IPasswordHealthReportApplicationRepository _passwordHealthReportApplicationRepo; + + public AddPasswordHealthReportApplicationCommand( + IOrganizationRepository organizationRepository, + IPasswordHealthReportApplicationRepository passwordHealthReportApplicationRepository) + { + _organizationRepo = organizationRepository; + _passwordHealthReportApplicationRepo = passwordHealthReportApplicationRepository; + } + + public async Task AddPasswordHealthReportApplicationAsync(AddPasswordHealthReportApplicationRequest request) + { + var (req, IsValid, errorMessage) = await ValidateRequestAsync(request); + if (!IsValid) + { + throw new BadRequestException(errorMessage); + } + + var passwordHealthReportApplication = new PasswordHealthReportApplication + { + OrganizationId = request.OrganizationId, + Uri = request.Url, + }; + + passwordHealthReportApplication.SetNewId(); + + var data = await _passwordHealthReportApplicationRepo.CreateAsync(passwordHealthReportApplication); + return data; + } + + public async Task> AddPasswordHealthReportApplicationAsync(IEnumerable requests) + { + var requestsList = requests.ToList(); + + // create tasks to validate each request + var tasks = requestsList.Select(async request => + { + var (req, IsValid, errorMessage) = await ValidateRequestAsync(request); + if (!IsValid) + { + throw new BadRequestException(errorMessage); + } + }); + + // run validations and allow exceptions to bubble + await Task.WhenAll(tasks); + + // create PasswordHealthReportApplication entities + var passwordHealthReportApplications = requestsList.Select(request => + { + var pwdHealthReportApplication = new PasswordHealthReportApplication + { + OrganizationId = request.OrganizationId, + Uri = request.Url, + }; + pwdHealthReportApplication.SetNewId(); + return pwdHealthReportApplication; + }); + + // create and return the entities + var response = new List(); + foreach (var record in passwordHealthReportApplications) + { + var data = await _passwordHealthReportApplicationRepo.CreateAsync(record); + response.Add(data); + } + + return response; + } + + private async Task> ValidateRequestAsync( + AddPasswordHealthReportApplicationRequest request) + { + // verify that the organization exists + var organization = await _organizationRepo.GetByIdAsync(request.OrganizationId); + if (organization == null) + { + return new Tuple(request, false, "Invalid Organization"); + } + + // ensure that we have a URL + if (string.IsNullOrWhiteSpace(request.Url)) + { + return new Tuple(request, false, "URL is required"); + } + + return new Tuple(request, true, string.Empty); + } +} diff --git a/src/Core/Tools/ReportFeatures/GetPasswordHealthReportApplicationQuery.cs b/src/Core/Tools/ReportFeatures/GetPasswordHealthReportApplicationQuery.cs new file mode 100644 index 000000000..5baf5b2f7 --- /dev/null +++ b/src/Core/Tools/ReportFeatures/GetPasswordHealthReportApplicationQuery.cs @@ -0,0 +1,27 @@ +using Bit.Core.Exceptions; +using Bit.Core.Tools.Entities; +using Bit.Core.Tools.ReportFeatures.Interfaces; +using Bit.Core.Tools.Repositories; + +namespace Bit.Core.Tools.ReportFeatures; + +public class GetPasswordHealthReportApplicationQuery : IGetPasswordHealthReportApplicationQuery +{ + private IPasswordHealthReportApplicationRepository _passwordHealthReportApplicationRepo; + + public GetPasswordHealthReportApplicationQuery( + IPasswordHealthReportApplicationRepository passwordHealthReportApplicationRepo) + { + _passwordHealthReportApplicationRepo = passwordHealthReportApplicationRepo; + } + + public async Task> GetPasswordHealthReportApplicationAsync(Guid organizationId) + { + if (organizationId == Guid.Empty) + { + throw new BadRequestException("OrganizationId is required."); + } + + return await _passwordHealthReportApplicationRepo.GetByOrganizationIdAsync(organizationId); + } +} diff --git a/src/Core/Tools/ReportFeatures/Interfaces/IAddPasswordHealthReportApplicationCommand.cs b/src/Core/Tools/ReportFeatures/Interfaces/IAddPasswordHealthReportApplicationCommand.cs new file mode 100644 index 000000000..86e7d44c7 --- /dev/null +++ b/src/Core/Tools/ReportFeatures/Interfaces/IAddPasswordHealthReportApplicationCommand.cs @@ -0,0 +1,10 @@ +using Bit.Core.Tools.Entities; +using Bit.Core.Tools.Requests; + +namespace Bit.Core.Tools.ReportFeatures.Interfaces; + +public interface IAddPasswordHealthReportApplicationCommand +{ + Task AddPasswordHealthReportApplicationAsync(AddPasswordHealthReportApplicationRequest request); + Task> AddPasswordHealthReportApplicationAsync(IEnumerable requests); +} diff --git a/src/Core/Tools/ReportFeatures/Interfaces/IGetPasswordHealthReportApplicationQuery.cs b/src/Core/Tools/ReportFeatures/Interfaces/IGetPasswordHealthReportApplicationQuery.cs new file mode 100644 index 000000000..f24119c2b --- /dev/null +++ b/src/Core/Tools/ReportFeatures/Interfaces/IGetPasswordHealthReportApplicationQuery.cs @@ -0,0 +1,8 @@ +using Bit.Core.Tools.Entities; + +namespace Bit.Core.Tools.ReportFeatures.Interfaces; + +public interface IGetPasswordHealthReportApplicationQuery +{ + Task> GetPasswordHealthReportApplicationAsync(Guid organizationId); +} diff --git a/src/Core/Tools/ReportFeatures/ReportingServiceCollectionExtensions.cs b/src/Core/Tools/ReportFeatures/ReportingServiceCollectionExtensions.cs index 5c813b8cb..85e5388a0 100644 --- a/src/Core/Tools/ReportFeatures/ReportingServiceCollectionExtensions.cs +++ b/src/Core/Tools/ReportFeatures/ReportingServiceCollectionExtensions.cs @@ -1,4 +1,4 @@ - +using Bit.Core.Tools.ReportFeatures.Interfaces; using Bit.Core.Tools.ReportFeatures.OrganizationReportMembers.Interfaces; using Microsoft.Extensions.DependencyInjection; @@ -9,5 +9,7 @@ public static class ReportingServiceCollectionExtensions public static void AddReportingServices(this IServiceCollection services) { services.AddScoped(); + services.AddScoped(); + services.AddScoped(); } } diff --git a/src/Core/Tools/Requests/AddPasswordHealthReportApplicationRequest.cs b/src/Core/Tools/Requests/AddPasswordHealthReportApplicationRequest.cs new file mode 100644 index 000000000..f5a1d35ea --- /dev/null +++ b/src/Core/Tools/Requests/AddPasswordHealthReportApplicationRequest.cs @@ -0,0 +1,7 @@ +namespace Bit.Core.Tools.Requests; + +public class AddPasswordHealthReportApplicationRequest +{ + public Guid OrganizationId { get; set; } + public string Url { get; set; } +} diff --git a/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs b/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs index ad0b46277..0e33895bf 100644 --- a/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs +++ b/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs @@ -95,6 +95,7 @@ public static class EntityFrameworkServiceCollectionExtensions services.AddSingleton(); services .AddSingleton(); + services.AddSingleton(); if (selfHosted) { diff --git a/test/Api.Test/Tools/Controllers/ReportsControllerTests.cs b/test/Api.Test/Tools/Controllers/ReportsControllerTests.cs new file mode 100644 index 000000000..07d37f672 --- /dev/null +++ b/test/Api.Test/Tools/Controllers/ReportsControllerTests.cs @@ -0,0 +1,49 @@ +using Bit.Api.Tools.Controllers; +using Bit.Core.Context; +using Bit.Core.Exceptions; +using Bit.Core.Tools.ReportFeatures.Interfaces; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Api.Test.Tools.Controllers; + + +[ControllerCustomize(typeof(ReportsController))] +[SutProviderCustomize] +public class ReportsControllerTests +{ + [Theory, BitAutoData] + public async Task GetPasswordHealthReportApplicationAsync_Success(SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency().AccessReports(Arg.Any()).Returns(true); + + // Act + var orgId = Guid.NewGuid(); + var result = await sutProvider.Sut.GetPasswordHealthReportApplications(orgId); + + // Assert + _ = sutProvider.GetDependency() + .Received(1) + .GetPasswordHealthReportApplicationAsync(Arg.Is(_ => _ == orgId)); + } + + [Theory, BitAutoData] + public async Task GetPasswordHealthReportApplicationAsync_withoutAccess(SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency().AccessReports(Arg.Any()).Returns(false); + + // Act & Assert + var orgId = Guid.NewGuid(); + await Assert.ThrowsAsync(async () => await sutProvider.Sut.GetPasswordHealthReportApplications(orgId)); + + // Assert + _ = sutProvider.GetDependency() + .Received(0); + } + + +} diff --git a/test/Core.Test/Tools/ReportFeatures/AddPasswordHealthReportApplicationCommandTests.cs b/test/Core.Test/Tools/ReportFeatures/AddPasswordHealthReportApplicationCommandTests.cs new file mode 100644 index 000000000..8c3a68fdc --- /dev/null +++ b/test/Core.Test/Tools/ReportFeatures/AddPasswordHealthReportApplicationCommandTests.cs @@ -0,0 +1,149 @@ +using AutoFixture; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Exceptions; +using Bit.Core.Repositories; +using Bit.Core.Tools.Entities; +using Bit.Core.Tools.ReportFeatures; +using Bit.Core.Tools.Repositories; +using Bit.Core.Tools.Requests; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.Tools.ReportFeatures; + +[SutProviderCustomize] +public class AddPasswordHealthReportApplicationCommandTests +{ + [Theory] + [BitAutoData] + public async Task AddPasswordHealthReportApplicationAsync_WithValidRequest_ShouldReturnPasswordHealthReportApplication( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var request = fixture.Create(); + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(fixture.Create()); + + sutProvider.GetDependency() + .CreateAsync(Arg.Any()) + .Returns(c => c.Arg()); + + // Act + var result = await sutProvider.Sut.AddPasswordHealthReportApplicationAsync(request); + + // Assert + Assert.NotNull(result); + } + + [Theory] + [BitAutoData] + public async Task AddPasswordHealthReportApplicationAsync_WithInvalidOrganizationId_ShouldThrowError( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var request = fixture.Create(); + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(null as Organization); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => await sutProvider.Sut.AddPasswordHealthReportApplicationAsync(request)); + Assert.Equal("Invalid Organization", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task AddPasswordHealthReportApplicationAsync_WithInvalidUrl_ShouldThrowError( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var request = fixture.Build() + .Without(_ => _.Url) + .Create(); + + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(fixture.Create()); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => await sutProvider.Sut.AddPasswordHealthReportApplicationAsync(request)); + Assert.Equal("URL is required", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task AddPasswordHealthReportApplicationAsync_Multiples_WithInvalidOrganizationId_ShouldThrowError( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var request = fixture.Build() + .Without(_ => _.OrganizationId) + .CreateMany(2).ToList(); + + request[0].OrganizationId = Guid.NewGuid(); + request[1].OrganizationId = Guid.Empty; + + // only return an organization for the first request and null for the second + sutProvider.GetDependency() + .GetByIdAsync(Arg.Is(x => x == request[0].OrganizationId)) + .Returns(fixture.Create()); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => await sutProvider.Sut.AddPasswordHealthReportApplicationAsync(request)); + Assert.Equal("Invalid Organization", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task AddPasswordHealthReportApplicationAsync_Multiples_WithInvalidUrl_ShouldThrowError( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var request = fixture.Build() + .CreateMany(2).ToList(); + + request[1].Url = string.Empty; + + // return an organization for both requests + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(fixture.Create()); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => await sutProvider.Sut.AddPasswordHealthReportApplicationAsync(request)); + Assert.Equal("URL is required", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task AddPasswordHealthReportApplicationAsync_Multiples_WithValidRequest_ShouldBeSuccessful( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var request = fixture.CreateMany(2); + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(fixture.Create()); + + sutProvider.GetDependency() + .CreateAsync(Arg.Any()) + .Returns(c => c.Arg()); + + // Act + var result = await sutProvider.Sut.AddPasswordHealthReportApplicationAsync(request); + + // Assert + Assert.True(result.Count() == 2); + sutProvider.GetDependency().Received(2); + sutProvider.GetDependency().Received(2); + } +} diff --git a/test/Core.Test/Tools/ReportFeatures/GetPasswordHealthReportApplicationQueryTests.cs b/test/Core.Test/Tools/ReportFeatures/GetPasswordHealthReportApplicationQueryTests.cs new file mode 100644 index 000000000..c4f098b0c --- /dev/null +++ b/test/Core.Test/Tools/ReportFeatures/GetPasswordHealthReportApplicationQueryTests.cs @@ -0,0 +1,53 @@ +using AutoFixture; +using Bit.Core.Exceptions; +using Bit.Core.Tools.Entities; +using Bit.Core.Tools.ReportFeatures; +using Bit.Core.Tools.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.Tools.ReportFeatures; + +[SutProviderCustomize] +public class GetPasswordHealthReportApplicationQueryTests +{ + [Theory] + [BitAutoData] + public async Task GetPasswordHealthReportApplicationAsync_WithValidOrganizationId_ShouldReturnPasswordHealthReportApplication( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var organizationId = fixture.Create(); + sutProvider.GetDependency() + .GetByOrganizationIdAsync(Arg.Any()) + .Returns(fixture.CreateMany(2).ToList()); + + // Act + var result = await sutProvider.Sut.GetPasswordHealthReportApplicationAsync(organizationId); + + // Assert + Assert.NotNull(result); + Assert.True(result.Count() == 2); + } + + [Theory] + [BitAutoData] + public async Task GetPasswordHealthReportApplicationAsync_WithInvalidOrganizationId_ShouldFail( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + sutProvider.GetDependency() + .GetByOrganizationIdAsync(Arg.Is(x => x == Guid.Empty)) + .Returns(new List()); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => await sutProvider.Sut.GetPasswordHealthReportApplicationAsync(Guid.Empty)); + + // Assert + Assert.Equal("OrganizationId is required.", exception.Message); + } +} From eec4a77bdaf237a4bed10db51feb63704de3a8fc Mon Sep 17 00:00:00 2001 From: Matt Bishop Date: Mon, 11 Nov 2024 13:19:20 -0500 Subject: [PATCH 21/29] Check run earlier during setup (#5022) --- .github/workflows/build.yml | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f83b03b16..1d092d8b4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -21,6 +21,8 @@ jobs: lint: name: Lint runs-on: ubuntu-22.04 + needs: + - check-run steps: - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -38,7 +40,6 @@ jobs: runs-on: ubuntu-22.04 needs: - lint - - check-run strategy: fail-fast: false matrix: @@ -132,7 +133,6 @@ jobs: security-events: write needs: - build-artifacts - - check-run strategy: fail-fast: false matrix: @@ -478,7 +478,8 @@ jobs: build-mssqlmigratorutility: name: Build MSSQL migrator utility runs-on: ubuntu-22.04 - needs: lint + needs: + - lint defaults: run: shell: bash @@ -531,7 +532,8 @@ jobs: name: Trigger self-host build if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main' runs-on: ubuntu-22.04 - needs: build-docker + needs: + - build-docker steps: - name: Log in to Azure - CI subscription uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 @@ -564,7 +566,8 @@ jobs: name: Trigger k8s deploy if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main' runs-on: ubuntu-22.04 - needs: build-docker + needs: + - build-docker steps: - name: Log in to Azure - CI subscription uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 @@ -600,7 +603,8 @@ jobs: github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'ephemeral-environment') runs-on: ubuntu-24.04 - needs: build-docker + needs: + - build-docker steps: - name: Log in to Azure - CI subscription uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 From db5beb54b50b178704ec6d1ebb9d60bb0ff4dda3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 11 Nov 2024 19:53:33 +0100 Subject: [PATCH 22/29] [deps] Tools: Update aws-sdk-net monorepo (#5017) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> --- src/Core/Core.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 338b908da..913c4b387 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -21,8 +21,8 @@ - - + + From 702a81b161ce125477e7fba0701ba7662cc530b8 Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Mon, 11 Nov 2024 13:07:21 -0800 Subject: [PATCH 23/29] [PM-14418] Add security-tasks feature flag (#5023) --- src/Core/Constants.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 8e759e143..f75920749 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -154,6 +154,7 @@ public static class FeatureFlagKeys public const string IntegrationPage = "pm-14505-admin-console-integration-page"; public const string NewDeviceVerificationTemporaryDismiss = "new-device-temporary-dismiss"; public const string NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss"; + public const string SecurityTasks = "security-tasks"; public static List GetAllKeys() { From 25afd50ab4f804a01e05024fd82aebcd2428fdc9 Mon Sep 17 00:00:00 2001 From: Jonas Hendrickx Date: Tue, 12 Nov 2024 14:53:34 +0100 Subject: [PATCH 24/29] [PM-14798] Update ProviderEventService for multi-organization enterprises (#5026) --- .../Implementations/ProviderEventService.cs | 85 +++++++------------ .../Services/ProviderEventServiceTests.cs | 49 ----------- 2 files changed, 29 insertions(+), 105 deletions(-) diff --git a/src/Billing/Services/Implementations/ProviderEventService.cs b/src/Billing/Services/Implementations/ProviderEventService.cs index 7ce72526c..548ed9f54 100644 --- a/src/Billing/Services/Implementations/ProviderEventService.cs +++ b/src/Billing/Services/Implementations/ProviderEventService.cs @@ -1,7 +1,6 @@ using Bit.Billing.Constants; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Entities; -using Bit.Core.Billing.Enums; using Bit.Core.Billing.Repositories; using Bit.Core.Enums; using Bit.Core.Utilities; @@ -42,78 +41,52 @@ public class ProviderEventService( case HandledStripeWebhook.InvoiceCreated: { var clients = - (await providerOrganizationRepository.GetManyDetailsByProviderAsync(parsedProviderId)) - .Where(providerOrganization => providerOrganization.Status == OrganizationStatusType.Managed); + await providerOrganizationRepository.GetManyDetailsByProviderAsync(parsedProviderId); var providerPlans = await providerPlanRepository.GetByProviderId(parsedProviderId); - var enterpriseProviderPlan = - providerPlans.FirstOrDefault(providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly); + var invoiceItems = new List(); - var teamsProviderPlan = - providerPlans.FirstOrDefault(providerPlan => providerPlan.PlanType == PlanType.TeamsMonthly); - - if (enterpriseProviderPlan == null || !enterpriseProviderPlan.IsConfigured() || - teamsProviderPlan == null || !teamsProviderPlan.IsConfigured()) + foreach (var client in clients) { - logger.LogError("Provider {ProviderID} is missing or has misconfigured provider plans", parsedProviderId); + if (client.Status != OrganizationStatusType.Managed) + { + continue; + } - throw new Exception("Cannot record invoice line items for Provider with missing or misconfigured provider plans"); - } + var plan = StaticStore.Plans.Single(x => x.Name == client.Plan && providerPlans.Any(y => y.PlanType == x.Type)); - var enterprisePlan = StaticStore.GetPlan(PlanType.EnterpriseMonthly); + var discountedPercentage = (100 - (invoice.Discount?.Coupon?.PercentOff ?? 0)) / 100; - var teamsPlan = StaticStore.GetPlan(PlanType.TeamsMonthly); - - var discountedPercentage = (100 - (invoice.Discount?.Coupon?.PercentOff ?? 0)) / 100; - - var discountedEnterpriseSeatPrice = enterprisePlan.PasswordManager.ProviderPortalSeatPrice * discountedPercentage; - - var discountedTeamsSeatPrice = teamsPlan.PasswordManager.ProviderPortalSeatPrice * discountedPercentage; - - var invoiceItems = clients.Select(client => new ProviderInvoiceItem - { - ProviderId = parsedProviderId, - InvoiceId = invoice.Id, - InvoiceNumber = invoice.Number, - ClientId = client.OrganizationId, - ClientName = client.OrganizationName, - PlanName = client.Plan, - AssignedSeats = client.Seats ?? 0, - UsedSeats = client.OccupiedSeats ?? 0, - Total = client.Plan == enterprisePlan.Name - ? (client.Seats ?? 0) * discountedEnterpriseSeatPrice - : (client.Seats ?? 0) * discountedTeamsSeatPrice - }).ToList(); - - if (enterpriseProviderPlan.PurchasedSeats is null or 0) - { - var enterpriseClientSeats = invoiceItems - .Where(item => item.PlanName == enterprisePlan.Name) - .Sum(item => item.AssignedSeats); - - var unassignedEnterpriseSeats = enterpriseProviderPlan.SeatMinimum - enterpriseClientSeats ?? 0; + var discountedSeatPrice = plan.PasswordManager.ProviderPortalSeatPrice * discountedPercentage; invoiceItems.Add(new ProviderInvoiceItem { ProviderId = parsedProviderId, InvoiceId = invoice.Id, InvoiceNumber = invoice.Number, - ClientName = "Unassigned seats", - PlanName = enterprisePlan.Name, - AssignedSeats = unassignedEnterpriseSeats, - UsedSeats = 0, - Total = unassignedEnterpriseSeats * discountedEnterpriseSeatPrice + ClientId = client.OrganizationId, + ClientName = client.OrganizationName, + PlanName = client.Plan, + AssignedSeats = client.Seats ?? 0, + UsedSeats = client.OccupiedSeats ?? 0, + Total = (client.Seats ?? 0) * discountedSeatPrice }); } - if (teamsProviderPlan.PurchasedSeats is null or 0) + foreach (var providerPlan in providerPlans.Where(x => x.PurchasedSeats is null or 0)) { - var teamsClientSeats = invoiceItems - .Where(item => item.PlanName == teamsPlan.Name) + var plan = StaticStore.GetPlan(providerPlan.PlanType); + + var clientSeats = invoiceItems + .Where(item => item.PlanName == plan.Name) .Sum(item => item.AssignedSeats); - var unassignedTeamsSeats = teamsProviderPlan.SeatMinimum - teamsClientSeats ?? 0; + var unassignedSeats = providerPlan.SeatMinimum - clientSeats ?? 0; + + var discountedPercentage = (100 - (invoice.Discount?.Coupon?.PercentOff ?? 0)) / 100; + + var discountedSeatPrice = plan.PasswordManager.ProviderPortalSeatPrice * discountedPercentage; invoiceItems.Add(new ProviderInvoiceItem { @@ -121,10 +94,10 @@ public class ProviderEventService( InvoiceId = invoice.Id, InvoiceNumber = invoice.Number, ClientName = "Unassigned seats", - PlanName = teamsPlan.Name, - AssignedSeats = unassignedTeamsSeats, + PlanName = plan.Name, + AssignedSeats = unassignedSeats, UsedSeats = 0, - Total = unassignedTeamsSeats * discountedTeamsSeatPrice + Total = unassignedSeats * discountedSeatPrice }); } diff --git a/test/Billing.Test/Services/ProviderEventServiceTests.cs b/test/Billing.Test/Services/ProviderEventServiceTests.cs index e76cf0d28..31f4ec896 100644 --- a/test/Billing.Test/Services/ProviderEventServiceTests.cs +++ b/test/Billing.Test/Services/ProviderEventServiceTests.cs @@ -8,7 +8,6 @@ using Bit.Core.Billing.Enums; using Bit.Core.Billing.Repositories; using Bit.Core.Enums; using Bit.Core.Utilities; -using FluentAssertions; using Microsoft.Extensions.Logging; using NSubstitute; using Stripe; @@ -89,54 +88,6 @@ public class ProviderEventServiceTests await _providerOrganizationRepository.DidNotReceiveWithAnyArgs().GetManyDetailsByProviderAsync(Arg.Any()); } - [Fact] - public async Task TryRecordInvoiceLineItems_InvoiceCreated_MisconfiguredProviderPlans_ThrowsException() - { - // Arrange - var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.InvoiceCreated); - - const string subscriptionId = "sub_1"; - var providerId = Guid.NewGuid(); - - var invoice = new Invoice - { - SubscriptionId = subscriptionId - }; - - _stripeEventService.GetInvoice(stripeEvent).Returns(invoice); - - var subscription = new Subscription - { - Metadata = new Dictionary { { "providerId", providerId.ToString() } } - }; - - _stripeFacade.GetSubscription(subscriptionId).Returns(subscription); - - var providerPlans = new List - { - new () - { - Id = Guid.NewGuid(), - ProviderId = providerId, - PlanType = PlanType.TeamsMonthly, - AllocatedSeats = 0, - PurchasedSeats = 0, - SeatMinimum = 100 - } - }; - - _providerPlanRepository.GetByProviderId(providerId).Returns(providerPlans); - - // Act - var function = async () => await _providerEventService.TryRecordInvoiceLineItems(stripeEvent); - - // Assert - await function - .Should() - .ThrowAsync() - .WithMessage("Cannot record invoice line items for Provider with missing or misconfigured provider plans"); - } - [Fact] public async Task TryRecordInvoiceLineItems_InvoiceCreated_Succeeds() { From f2bf9ea9f8bdea0a70741fe26a09ec432013f66f Mon Sep 17 00:00:00 2001 From: Jared McCannon Date: Tue, 12 Nov 2024 11:25:36 -0600 Subject: [PATCH 25/29] [PM-12479] - Adding group-details endpoint (#4959) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✨ Added group-details endpoint. Moved group auth handler to AdminConsole directory. --------- Co-authored-by: Matt Bishop --- .../Controllers/GroupsController.cs | 36 ++- .../OrganizationUsersController.cs | 1 + src/Api/Api.csproj | 1 - .../Utilities/ServiceCollectionExtensions.cs | 2 +- .../Groups/GroupAuthorizationHandler.cs | 62 ----- .../Groups/GroupOperations.cs | 22 -- .../GroupAuthorizationHandler.cs | 43 ++++ .../Groups/Authorization/GroupOperations.cs | 17 ++ ...tionUserUserDetailsAuthorizationHandler.cs | 1 + ...UserUserMiniDetailsAuthorizationHandler.cs | 10 +- .../Authorization/OrganizationScope.cs | 2 +- src/Core/Constants.cs | 1 + .../GroupAuthorizationHandlerTests.cs | 223 ++++++++++++++++++ .../GroupAuthorizationHandlerTests.cs | 125 ---------- ...serUserDetailsAuthorizationHandlerTests.cs | 1 + ...serMiniDetailsAuthorizationHandlerTests.cs | 1 + 16 files changed, 323 insertions(+), 225 deletions(-) delete mode 100644 src/Api/Vault/AuthorizationHandlers/Groups/GroupAuthorizationHandler.cs delete mode 100644 src/Api/Vault/AuthorizationHandlers/Groups/GroupOperations.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Groups/Authorization/GroupAuthorizationHandler.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Groups/Authorization/GroupOperations.cs rename src/Core/AdminConsole/OrganizationFeatures/{OrganizationUsers => Shared}/Authorization/OrganizationScope.cs (90%) create mode 100644 test/Api.Test/AdminConsole/AuthorizationHandlers/GroupAuthorizationHandlerTests.cs delete mode 100644 test/Api.Test/Vault/AuthorizationHandlers/GroupAuthorizationHandlerTests.cs diff --git a/src/Api/AdminConsole/Controllers/GroupsController.cs b/src/Api/AdminConsole/Controllers/GroupsController.cs index f3f1d343c..0e4665699 100644 --- a/src/Api/AdminConsole/Controllers/GroupsController.cs +++ b/src/Api/AdminConsole/Controllers/GroupsController.cs @@ -2,15 +2,16 @@ using Bit.Api.AdminConsole.Models.Response; using Bit.Api.Models.Response; using Bit.Api.Vault.AuthorizationHandlers.Collections; -using Bit.Api.Vault.AuthorizationHandlers.Groups; +using Bit.Core; +using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Authorization; using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces; +using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; using Bit.Core.Context; using Bit.Core.Exceptions; using Bit.Core.Repositories; using Bit.Core.Services; -using Bit.Core.Utilities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -89,11 +90,34 @@ public class GroupsController : Controller } [HttpGet("")] - public async Task> Get(Guid orgId) + public async Task> GetOrganizationGroups(Guid orgId) { - var authorized = - (await _authorizationService.AuthorizeAsync(User, GroupOperations.ReadAll(orgId))).Succeeded; - if (!authorized) + var authResult = await _authorizationService.AuthorizeAsync(User, new OrganizationScope(orgId), GroupOperations.ReadAll); + if (!authResult.Succeeded) + { + throw new NotFoundException(); + } + + if (_featureService.IsEnabled(FeatureFlagKeys.SecureOrgGroupDetails)) + { + var groups = await _groupRepository.GetManyByOrganizationIdAsync(orgId); + var responses = groups.Select(g => new GroupDetailsResponseModel(g, [])); + return new ListResponseModel(responses); + } + + var groupDetails = await _groupRepository.GetManyWithCollectionsByOrganizationIdAsync(orgId); + var detailResponses = groupDetails.Select(g => new GroupDetailsResponseModel(g.Item1, g.Item2)); + return new ListResponseModel(detailResponses); + } + + [HttpGet("details")] + public async Task> GetOrganizationGroupDetails(Guid orgId) + { + var authResult = _featureService.IsEnabled(FeatureFlagKeys.SecureOrgGroupDetails) + ? await _authorizationService.AuthorizeAsync(User, new OrganizationScope(orgId), GroupOperations.ReadAllDetails) + : await _authorizationService.AuthorizeAsync(User, new OrganizationScope(orgId), GroupOperations.ReadAll); + + if (!authResult.Succeeded) { throw new NotFoundException(); } diff --git a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs index b81cb068f..3193962fa 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs @@ -8,6 +8,7 @@ using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Repositories; diff --git a/src/Api/Api.csproj b/src/Api/Api.csproj index d6d055e90..4a018b219 100644 --- a/src/Api/Api.csproj +++ b/src/Api/Api.csproj @@ -1,5 +1,4 @@  - bitwarden-Api false diff --git a/src/Api/Utilities/ServiceCollectionExtensions.cs b/src/Api/Utilities/ServiceCollectionExtensions.cs index a7fbddbaa..8a58a5f23 100644 --- a/src/Api/Utilities/ServiceCollectionExtensions.cs +++ b/src/Api/Utilities/ServiceCollectionExtensions.cs @@ -1,5 +1,5 @@ using Bit.Api.Vault.AuthorizationHandlers.Collections; -using Bit.Api.Vault.AuthorizationHandlers.Groups; +using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Authorization; using Bit.Core.IdentityServer; using Bit.Core.Settings; using Bit.Core.Utilities; diff --git a/src/Api/Vault/AuthorizationHandlers/Groups/GroupAuthorizationHandler.cs b/src/Api/Vault/AuthorizationHandlers/Groups/GroupAuthorizationHandler.cs deleted file mode 100644 index 666cd725e..000000000 --- a/src/Api/Vault/AuthorizationHandlers/Groups/GroupAuthorizationHandler.cs +++ /dev/null @@ -1,62 +0,0 @@ -#nullable enable -using Bit.Core.Context; -using Microsoft.AspNetCore.Authorization; - -namespace Bit.Api.Vault.AuthorizationHandlers.Groups; - -/// -/// Handles authorization logic for Group operations. -/// This uses new logic implemented in the Flexible Collections initiative. -/// -public class GroupAuthorizationHandler : AuthorizationHandler -{ - private readonly ICurrentContext _currentContext; - - public GroupAuthorizationHandler(ICurrentContext currentContext) - { - _currentContext = currentContext; - } - - protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, - GroupOperationRequirement requirement) - { - // Acting user is not authenticated, fail - if (!_currentContext.UserId.HasValue) - { - context.Fail(); - return; - } - - if (requirement.OrganizationId == default) - { - context.Fail(); - return; - } - - var org = _currentContext.GetOrganization(requirement.OrganizationId); - - switch (requirement) - { - case not null when requirement.Name == nameof(GroupOperations.ReadAll): - await CanReadAllAsync(context, requirement, org); - break; - } - } - - private async Task CanReadAllAsync(AuthorizationHandlerContext context, GroupOperationRequirement requirement, - CurrentContextOrganization? org) - { - // All users of an organization can read all groups belonging to the organization for collection access management - if (org is not null) - { - context.Succeed(requirement); - return; - } - - // Allow provider users to read all groups if they are a provider for the target organization - if (await _currentContext.ProviderUserForOrgAsync(requirement.OrganizationId)) - { - context.Succeed(requirement); - } - } -} diff --git a/src/Api/Vault/AuthorizationHandlers/Groups/GroupOperations.cs b/src/Api/Vault/AuthorizationHandlers/Groups/GroupOperations.cs deleted file mode 100644 index 7735bd52a..000000000 --- a/src/Api/Vault/AuthorizationHandlers/Groups/GroupOperations.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Microsoft.AspNetCore.Authorization.Infrastructure; - -namespace Bit.Api.Vault.AuthorizationHandlers.Groups; - -public class GroupOperationRequirement : OperationAuthorizationRequirement -{ - public Guid OrganizationId { get; init; } - - public GroupOperationRequirement(string name, Guid organizationId) - { - Name = name; - OrganizationId = organizationId; - } -} - -public static class GroupOperations -{ - public static GroupOperationRequirement ReadAll(Guid organizationId) - { - return new GroupOperationRequirement(nameof(ReadAll), organizationId); - } -} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Groups/Authorization/GroupAuthorizationHandler.cs b/src/Core/AdminConsole/OrganizationFeatures/Groups/Authorization/GroupAuthorizationHandler.cs new file mode 100644 index 000000000..d531c1175 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Groups/Authorization/GroupAuthorizationHandler.cs @@ -0,0 +1,43 @@ +#nullable enable +using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization; +using Bit.Core.Context; +using Bit.Core.Enums; +using Microsoft.AspNetCore.Authorization; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Groups.Authorization; + +public class GroupAuthorizationHandler(ICurrentContext currentContext) + : AuthorizationHandler +{ + protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, + GroupOperationRequirement requirement, OrganizationScope organizationScope) + { + var authorized = requirement switch + { + not null when requirement.Name == nameof(GroupOperations.ReadAll) => + await CanReadAllAsync(organizationScope), + not null when requirement.Name == nameof(GroupOperations.ReadAllDetails) => + await CanViewGroupDetailsAsync(organizationScope), + _ => false + }; + + if (requirement is not null && authorized) + { + context.Succeed(requirement); + } + } + + private async Task CanReadAllAsync(OrganizationScope organizationScope) => + currentContext.GetOrganization(organizationScope) is not null + || await currentContext.ProviderUserForOrgAsync(organizationScope); + + private async Task CanViewGroupDetailsAsync(OrganizationScope organizationScope) => + currentContext.GetOrganization(organizationScope) is + { Type: OrganizationUserType.Owner } or + { Type: OrganizationUserType.Admin } or + { + Permissions: { ManageGroups: true } or + { ManageUsers: true } + } || + await currentContext.ProviderUserForOrgAsync(organizationScope); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Groups/Authorization/GroupOperations.cs b/src/Core/AdminConsole/OrganizationFeatures/Groups/Authorization/GroupOperations.cs new file mode 100644 index 000000000..f5b3b975f --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Groups/Authorization/GroupOperations.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.Authorization.Infrastructure; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Groups.Authorization; + +public class GroupOperationRequirement : OperationAuthorizationRequirement +{ + public GroupOperationRequirement(string name) + { + Name = name; + } +} + +public static class GroupOperations +{ + public static readonly GroupOperationRequirement ReadAll = new(nameof(ReadAll)); + public static readonly GroupOperationRequirement ReadAllDetails = new(nameof(ReadAllDetails)); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Authorization/OrganizationUserUserDetailsAuthorizationHandler.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Authorization/OrganizationUserUserDetailsAuthorizationHandler.cs index e890e4d9f..f57850e64 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Authorization/OrganizationUserUserDetailsAuthorizationHandler.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Authorization/OrganizationUserUserDetailsAuthorizationHandler.cs @@ -1,4 +1,5 @@ #nullable enable +using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization; using Bit.Core.Context; using Bit.Core.Enums; using Microsoft.AspNetCore.Authorization; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Authorization/OrganizationUserUserMiniDetailsAuthorizationHandler.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Authorization/OrganizationUserUserMiniDetailsAuthorizationHandler.cs index 24d9d0c26..01b77a05b 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Authorization/OrganizationUserUserMiniDetailsAuthorizationHandler.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Authorization/OrganizationUserUserMiniDetailsAuthorizationHandler.cs @@ -1,5 +1,5 @@ -using Bit.Core.Context; -using Bit.Core.Services; +using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization; +using Bit.Core.Context; using Microsoft.AspNetCore.Authorization; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization; @@ -7,14 +7,10 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authoriza public class OrganizationUserUserMiniDetailsAuthorizationHandler : AuthorizationHandler { - private readonly IApplicationCacheService _applicationCacheService; private readonly ICurrentContext _currentContext; - public OrganizationUserUserMiniDetailsAuthorizationHandler( - IApplicationCacheService applicationCacheService, - ICurrentContext currentContext) + public OrganizationUserUserMiniDetailsAuthorizationHandler(ICurrentContext currentContext) { - _applicationCacheService = applicationCacheService; _currentContext = currentContext; } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Authorization/OrganizationScope.cs b/src/Core/AdminConsole/OrganizationFeatures/Shared/Authorization/OrganizationScope.cs similarity index 90% rename from src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Authorization/OrganizationScope.cs rename to src/Core/AdminConsole/OrganizationFeatures/Shared/Authorization/OrganizationScope.cs index 57856e702..689cff24e 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Authorization/OrganizationScope.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Shared/Authorization/OrganizationScope.cs @@ -1,6 +1,6 @@ #nullable enable -namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization; +namespace Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization; /// /// A typed wrapper for an organization Guid. This is used for authorization checks diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index f75920749..67d463bac 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -143,6 +143,7 @@ public static class FeatureFlagKeys public const string StorageReseedRefactor = "storage-reseed-refactor"; public const string TrialPayment = "PM-8163-trial-payment"; public const string RemoveServerVersionHeader = "remove-server-version-header"; + public const string SecureOrgGroupDetails = "pm-3479-secure-org-group-details"; public const string AccessIntelligence = "pm-13227-access-intelligence"; public const string VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint"; public const string PM12275_MultiOrganizationEnterprises = "pm-12275-multi-organization-enterprises"; diff --git a/test/Api.Test/AdminConsole/AuthorizationHandlers/GroupAuthorizationHandlerTests.cs b/test/Api.Test/AdminConsole/AuthorizationHandlers/GroupAuthorizationHandlerTests.cs new file mode 100644 index 000000000..a1b63c4fd --- /dev/null +++ b/test/Api.Test/AdminConsole/AuthorizationHandlers/GroupAuthorizationHandlerTests.cs @@ -0,0 +1,223 @@ +using System.Security.Claims; +using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Authorization; +using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization; +using Bit.Core.Context; +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.AspNetCore.Authorization; +using NSubstitute; +using Xunit; + +namespace Bit.Api.Test.AdminConsole.AuthorizationHandlers; + +[SutProviderCustomize] +public class GroupAuthorizationHandlerTests +{ + [Theory] + [BitAutoData(OrganizationUserType.Admin)] + [BitAutoData(OrganizationUserType.Owner)] + [BitAutoData(OrganizationUserType.User)] + [BitAutoData(OrganizationUserType.Custom)] + public async Task CanReadAllAsync_WhenMemberOfOrg_Success( + OrganizationUserType userType, + OrganizationScope scope, + Guid userId, SutProvider sutProvider, + CurrentContextOrganization organization) + { + organization.Type = userType; + organization.Permissions = new Permissions(); + + var context = new AuthorizationHandlerContext( + new[] { GroupOperations.ReadAll }, + new ClaimsPrincipal(), + scope); + + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency().GetOrganization(scope).Returns(organization); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasSucceeded); + } + + [Theory, BitAutoData] + public async Task CanReadAllAsync_WithProviderUser_Success( + Guid userId, + OrganizationScope scope, + SutProvider sutProvider, CurrentContextOrganization organization) + { + organization.Type = OrganizationUserType.User; + organization.Permissions = new Permissions(); + + var context = new AuthorizationHandlerContext( + new[] { GroupOperations.ReadAll }, + new ClaimsPrincipal(), + scope); + + sutProvider.GetDependency() + .UserId + .Returns(userId); + sutProvider.GetDependency() + .ProviderUserForOrgAsync(scope) + .Returns(true); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasSucceeded); + } + + [Theory, BitAutoData] + public async Task CanReadAllAsync_WhenMissingOrgAccess_NoSuccess( + Guid userId, + OrganizationScope scope, + SutProvider sutProvider) + { + + var context = new AuthorizationHandlerContext( + new[] { GroupOperations.ReadAll }, + new ClaimsPrincipal(), + scope + ); + + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency().GetOrganization(Arg.Any()).Returns((CurrentContextOrganization)null); + sutProvider.GetDependency().ProviderUserForOrgAsync(Arg.Any()).Returns(false); + + await sutProvider.Sut.HandleAsync(context); + Assert.False(context.HasSucceeded); + } + + [Theory] + [BitAutoData(OrganizationUserType.Admin)] + [BitAutoData(OrganizationUserType.Owner)] + public async Task HandleRequirementsAsync_GivenViewDetailsOperation_WhenUserIsAdminOwner_ThenShouldSucceed(OrganizationUserType userType, + OrganizationScope scope, + CurrentContextOrganization organization, SutProvider sutProvider) + { + organization.Type = userType; + organization.Permissions = new Permissions(); + + var context = new AuthorizationHandlerContext( + [GroupOperations.ReadAllDetails], + new ClaimsPrincipal(), + scope + ); + + sutProvider.GetDependency().GetOrganization(scope).Returns(organization); + + await sutProvider.Sut.HandleAsync(context); + Assert.True(context.HasSucceeded); + } + + [Theory] + [BitAutoData(OrganizationUserType.User)] + [BitAutoData(OrganizationUserType.Custom)] + public async Task HandleRequirementsAsync_GivenViewDetailsOperation_WhenUserIsNotOwnerOrAdmin_ThenShouldFail(OrganizationUserType userType, + OrganizationScope scope, + CurrentContextOrganization organization, SutProvider sutProvider) + { + organization.Type = userType; + organization.Permissions = new Permissions(); + + var context = new AuthorizationHandlerContext( + [GroupOperations.ReadAllDetails], + new ClaimsPrincipal(), + scope + ); + + sutProvider.GetDependency().GetOrganization(scope).Returns(organization); + + await sutProvider.Sut.HandleAsync(context); + Assert.False(context.HasSucceeded); + } + + [Theory, BitAutoData] + public async Task HandleRequirementsAsync_GivenViewDetailsOperation_WhenUserHasManageGroupPermission_ThenShouldSucceed( + OrganizationScope scope, + CurrentContextOrganization organization, SutProvider sutProvider) + { + organization.Type = OrganizationUserType.Custom; + organization.Permissions = new Permissions + { + ManageGroups = true + }; + + var context = new AuthorizationHandlerContext( + [GroupOperations.ReadAllDetails], + new ClaimsPrincipal(), + scope + ); + + sutProvider.GetDependency().GetOrganization(scope).Returns(organization); + + await sutProvider.Sut.HandleAsync(context); + Assert.True(context.HasSucceeded); + } + + [Theory, BitAutoData] + public async Task HandleRequirementsAsync_GivenViewDetailsOperation_WhenUserHasManageUserPermission_ThenShouldSucceed( + OrganizationScope scope, + CurrentContextOrganization organization, SutProvider sutProvider) + { + organization.Type = OrganizationUserType.Custom; + organization.Permissions = new Permissions + { + ManageUsers = true + }; + + var context = new AuthorizationHandlerContext( + [GroupOperations.ReadAllDetails], + new ClaimsPrincipal(), + scope + ); + + sutProvider.GetDependency().GetOrganization(scope).Returns(organization); + + await sutProvider.Sut.HandleAsync(context); + Assert.True(context.HasSucceeded); + } + + [Theory, BitAutoData] + public async Task HandleRequirementsAsync_GivenViewDetailsOperation_WhenUserIsStandardUserTypeWithoutElevatedPermissions_ThenShouldFail( + OrganizationScope scope, + CurrentContextOrganization organization, SutProvider sutProvider) + { + organization.Type = OrganizationUserType.User; + organization.Permissions = new Permissions(); + + var context = new AuthorizationHandlerContext( + [GroupOperations.ReadAllDetails], + new ClaimsPrincipal(), + scope + ); + + sutProvider.GetDependency().GetOrganization(scope).Returns(organization); + sutProvider.GetDependency().ProviderUserForOrgAsync(scope).Returns(false); + + await sutProvider.Sut.HandleAsync(context); + Assert.False(context.HasSucceeded); + } + + [Theory, BitAutoData] + public async Task HandleRequirementsAsync_GivenViewDetailsOperation_WhenIsProviderUser_ThenShouldSucceed( + OrganizationScope scope, + SutProvider sutProvider, CurrentContextOrganization organization) + { + organization.Type = OrganizationUserType.User; + organization.Permissions = new Permissions(); + + var context = new AuthorizationHandlerContext( + new[] { GroupOperations.ReadAll }, + new ClaimsPrincipal(), + scope); + + sutProvider.GetDependency().GetOrganization(scope).Returns(organization); + sutProvider.GetDependency().ProviderUserForOrgAsync(scope).Returns(true); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasSucceeded); + } +} diff --git a/test/Api.Test/Vault/AuthorizationHandlers/GroupAuthorizationHandlerTests.cs b/test/Api.Test/Vault/AuthorizationHandlers/GroupAuthorizationHandlerTests.cs deleted file mode 100644 index 608e201c5..000000000 --- a/test/Api.Test/Vault/AuthorizationHandlers/GroupAuthorizationHandlerTests.cs +++ /dev/null @@ -1,125 +0,0 @@ -using System.Security.Claims; -using Bit.Api.Vault.AuthorizationHandlers.Groups; -using Bit.Core.Context; -using Bit.Core.Enums; -using Bit.Core.Models.Data; -using Bit.Test.Common.AutoFixture; -using Bit.Test.Common.AutoFixture.Attributes; -using Microsoft.AspNetCore.Authorization; -using NSubstitute; -using Xunit; - -namespace Bit.Api.Test.Vault.AuthorizationHandlers; - -[SutProviderCustomize] -public class GroupAuthorizationHandlerTests -{ - [Theory] - [BitAutoData(OrganizationUserType.Admin)] - [BitAutoData(OrganizationUserType.Owner)] - [BitAutoData(OrganizationUserType.User)] - [BitAutoData(OrganizationUserType.Custom)] - public async Task CanReadAllAsync_WhenMemberOfOrg_Success( - OrganizationUserType userType, - Guid userId, SutProvider sutProvider, - CurrentContextOrganization organization) - { - organization.Type = userType; - organization.Permissions = new Permissions(); - - var context = new AuthorizationHandlerContext( - new[] { GroupOperations.ReadAll(organization.Id) }, - new ClaimsPrincipal(), - null); - - sutProvider.GetDependency().UserId.Returns(userId); - sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); - - await sutProvider.Sut.HandleAsync(context); - - Assert.True(context.HasSucceeded); - } - - [Theory, BitAutoData] - public async Task CanReadAllAsync_WithProviderUser_Success( - Guid userId, - SutProvider sutProvider, CurrentContextOrganization organization) - { - organization.Type = OrganizationUserType.User; - organization.Permissions = new Permissions(); - - var context = new AuthorizationHandlerContext( - new[] { GroupOperations.ReadAll(organization.Id) }, - new ClaimsPrincipal(), - null); - - sutProvider.GetDependency() - .UserId - .Returns(userId); - sutProvider.GetDependency() - .ProviderUserForOrgAsync(organization.Id) - .Returns(true); - - await sutProvider.Sut.HandleAsync(context); - - Assert.True(context.HasSucceeded); - } - - [Theory, BitAutoData] - public async Task CanReadAllAsync_WhenMissingOrgAccess_NoSuccess( - Guid userId, - CurrentContextOrganization organization, - SutProvider sutProvider) - { - - var context = new AuthorizationHandlerContext( - new[] { GroupOperations.ReadAll(organization.Id) }, - new ClaimsPrincipal(), - null - ); - - sutProvider.GetDependency().UserId.Returns(userId); - sutProvider.GetDependency().GetOrganization(Arg.Any()).Returns((CurrentContextOrganization)null); - sutProvider.GetDependency().ProviderUserForOrgAsync(Arg.Any()).Returns(false); - - await sutProvider.Sut.HandleAsync(context); - Assert.False(context.HasSucceeded); - } - - [Theory, BitAutoData] - public async Task HandleRequirementAsync_MissingUserId_Failure( - Guid organizationId, - SutProvider sutProvider) - { - var context = new AuthorizationHandlerContext( - new[] { GroupOperations.ReadAll(organizationId) }, - new ClaimsPrincipal(), - null - ); - - // Simulate missing user id - sutProvider.GetDependency().UserId.Returns((Guid?)null); - - await sutProvider.Sut.HandleAsync(context); - Assert.False(context.HasSucceeded); - Assert.True(context.HasFailed); - } - - [Theory, BitAutoData] - public async Task HandleRequirementAsync_NoSpecifiedOrgId_Failure( - SutProvider sutProvider) - { - var context = new AuthorizationHandlerContext( - new[] { GroupOperations.ReadAll(default) }, - new ClaimsPrincipal(), - null - ); - - sutProvider.GetDependency().UserId.Returns(new Guid()); - - await sutProvider.Sut.HandleAsync(context); - - Assert.False(context.HasSucceeded); - Assert.True(context.HasFailed); - } -} diff --git a/test/Core.Test/AdminConsole/Authorization/OrganizationUserUserDetailsAuthorizationHandlerTests.cs b/test/Core.Test/AdminConsole/Authorization/OrganizationUserUserDetailsAuthorizationHandlerTests.cs index 4a3a7f647..0814754f8 100644 --- a/test/Core.Test/AdminConsole/Authorization/OrganizationUserUserDetailsAuthorizationHandlerTests.cs +++ b/test/Core.Test/AdminConsole/Authorization/OrganizationUserUserDetailsAuthorizationHandlerTests.cs @@ -1,5 +1,6 @@ using System.Security.Claims; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization; +using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Test.AdminConsole.AutoFixture; diff --git a/test/Core.Test/AdminConsole/Authorization/OrganizationUserUserMiniDetailsAuthorizationHandlerTests.cs b/test/Core.Test/AdminConsole/Authorization/OrganizationUserUserMiniDetailsAuthorizationHandlerTests.cs index c74cb2d5c..6732486a5 100644 --- a/test/Core.Test/AdminConsole/Authorization/OrganizationUserUserMiniDetailsAuthorizationHandlerTests.cs +++ b/test/Core.Test/AdminConsole/Authorization/OrganizationUserUserMiniDetailsAuthorizationHandlerTests.cs @@ -1,5 +1,6 @@ using System.Security.Claims; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization; +using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Test.AdminConsole.AutoFixture; From a26ba3b3309d37ea0bd33fef0f1c77a9f5a98a8c Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Tue, 12 Nov 2024 14:11:10 -0500 Subject: [PATCH 26/29] [PM-14401] Scale MSP on Admin client organization update (#5001) * Privatize GetAssignedSeatTotalAsync * Add SeatAdjustmentResultsInPurchase method * Move adjustment logic to ProviderClientsController.Update * Remove unused AssignSeatsToClientOrganization method * Alphabetize ProviderBillingService * Scale MSP on Admin client organization update * Run dotnet format * Patch build process * Rui's feedback --------- Co-authored-by: Matt Bishop --- .../Billing/ProviderBillingService.cs | 255 ++-- .../Billing/ProviderBillingServiceTests.cs | 1045 +++++++---------- .../Controllers/OrganizationsController.cs | 89 +- .../Controllers/BaseBillingController.cs | 4 +- .../Controllers/ProviderClientsController.cs | 22 +- .../Services/IProviderBillingService.cs | 50 +- src/Core/Constants.cs | 1 + .../OrganizationsControllerTests.cs | 343 ++++++ .../ProviderClientsControllerTests.cs | 109 +- test/Api.Test/Billing/Utilities.cs | 4 +- 10 files changed, 1047 insertions(+), 875 deletions(-) create mode 100644 test/Admin.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs diff --git a/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs b/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs index 7b1d33599..a6bf62871 100644 --- a/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs +++ b/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs @@ -2,17 +2,14 @@ using Bit.Commercial.Core.Billing.Models; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; -using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Entities; using Bit.Core.Billing.Enums; -using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Repositories; using Bit.Core.Billing.Services; using Bit.Core.Billing.Services.Contracts; -using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Business; @@ -27,7 +24,6 @@ using Stripe; namespace Bit.Commercial.Core.Billing; public class ProviderBillingService( - ICurrentContext currentContext, IGlobalSettings globalSettings, ILogger logger, IOrganizationRepository organizationRepository, @@ -35,38 +31,76 @@ public class ProviderBillingService( IProviderInvoiceItemRepository providerInvoiceItemRepository, IProviderOrganizationRepository providerOrganizationRepository, IProviderPlanRepository providerPlanRepository, - IProviderRepository providerRepository, IStripeAdapter stripeAdapter, ISubscriberService subscriberService) : IProviderBillingService { - public async Task AssignSeatsToClientOrganization( - Provider provider, - Organization organization, - int seats) + public async Task ChangePlan(ChangeProviderPlanCommand command) { - ArgumentNullException.ThrowIfNull(organization); + var plan = await providerPlanRepository.GetByIdAsync(command.ProviderPlanId); - if (seats < 0) + if (plan == null) { - throw new BillingException( - "You cannot assign negative seats to a client.", - "MSP cannot assign negative seats to a client organization"); + throw new BadRequestException("Provider plan not found."); } - if (seats == organization.Seats) + if (plan.PlanType == command.NewPlan) { - logger.LogWarning("Client organization ({ID}) already has {Seats} seats assigned to it", organization.Id, organization.Seats); - return; } - var seatAdjustment = seats - (organization.Seats ?? 0); + var oldPlanConfiguration = StaticStore.GetPlan(plan.PlanType); - await ScaleSeats(provider, organization.PlanType, seatAdjustment); + plan.PlanType = command.NewPlan; + await providerPlanRepository.ReplaceAsync(plan); - organization.Seats = seats; + Subscription subscription; + try + { + subscription = await stripeAdapter.ProviderSubscriptionGetAsync(command.GatewaySubscriptionId, plan.ProviderId); + } + catch (InvalidOperationException) + { + throw new ConflictException("Subscription not found."); + } - await organizationRepository.ReplaceAsync(organization); + var oldSubscriptionItem = subscription.Items.SingleOrDefault(x => + x.Price.Id == oldPlanConfiguration.PasswordManager.StripeProviderPortalSeatPlanId); + + var updateOptions = new SubscriptionUpdateOptions + { + Items = + [ + new SubscriptionItemOptions + { + Price = StaticStore.GetPlan(command.NewPlan).PasswordManager.StripeProviderPortalSeatPlanId, + Quantity = oldSubscriptionItem!.Quantity + }, + new SubscriptionItemOptions + { + Id = oldSubscriptionItem.Id, + Deleted = true + } + ] + }; + + await stripeAdapter.SubscriptionUpdateAsync(command.GatewaySubscriptionId, updateOptions); + + // Refactor later to ?ChangeClientPlanCommand? (ProviderPlanId, ProviderId, OrganizationId) + // 1. Retrieve PlanType and PlanName for ProviderPlan + // 2. Assign PlanType & PlanName to Organization + var providerOrganizations = await providerOrganizationRepository.GetManyDetailsByProviderAsync(plan.ProviderId); + + foreach (var providerOrganization in providerOrganizations) + { + var organization = await organizationRepository.GetByIdAsync(providerOrganization.OrganizationId); + if (organization == null) + { + throw new ConflictException($"Organization '{providerOrganization.Id}' not found."); + } + organization.PlanType = command.NewPlan; + organization.Plan = StaticStore.GetPlan(command.NewPlan).Name; + await organizationRepository.ReplaceAsync(organization); + } } public async Task CreateCustomerForClientOrganization( @@ -171,65 +205,16 @@ public class ProviderBillingService( return memoryStream.ToArray(); } - public async Task GetAssignedSeatTotalForPlanOrThrow( - Guid providerId, - PlanType planType) - { - var provider = await providerRepository.GetByIdAsync(providerId); - - if (provider == null) - { - logger.LogError( - "Could not find provider ({ID}) when retrieving assigned seat total", - providerId); - - throw new BillingException(); - } - - if (provider.Type == ProviderType.Reseller) - { - logger.LogError("Assigned seats cannot be retrieved for reseller-type provider ({ID})", providerId); - - throw new BillingException(); - } - - var providerOrganizations = await providerOrganizationRepository.GetManyDetailsByProviderAsync(providerId); - - var plan = StaticStore.GetPlan(planType); - - return providerOrganizations - .Where(providerOrganization => providerOrganization.Plan == plan.Name && providerOrganization.Status == OrganizationStatusType.Managed) - .Sum(providerOrganization => providerOrganization.Seats ?? 0); - } - public async Task ScaleSeats( Provider provider, PlanType planType, int seatAdjustment) { - ArgumentNullException.ThrowIfNull(provider); + var providerPlan = await GetProviderPlanAsync(provider, planType); - if (!provider.SupportsConsolidatedBilling()) - { - logger.LogError("Provider ({ProviderID}) cannot scale their seats", provider.Id); + var seatMinimum = providerPlan.SeatMinimum ?? 0; - throw new BillingException(); - } - - var providerPlans = await providerPlanRepository.GetByProviderId(provider.Id); - - var providerPlan = providerPlans.FirstOrDefault(providerPlan => providerPlan.PlanType == planType); - - if (providerPlan == null || !providerPlan.IsConfigured()) - { - logger.LogError("Cannot scale provider ({ProviderID}) seats for plan type {PlanType} when their matching provider plan is not configured", provider.Id, planType); - - throw new BillingException(); - } - - var seatMinimum = providerPlan.SeatMinimum.GetValueOrDefault(0); - - var currentlyAssignedSeatTotal = await GetAssignedSeatTotalForPlanOrThrow(provider.Id, planType); + var currentlyAssignedSeatTotal = await GetAssignedSeatTotalAsync(provider, planType); var newlyAssignedSeatTotal = currentlyAssignedSeatTotal + seatAdjustment; @@ -256,13 +241,6 @@ public class ProviderBillingService( else if (currentlyAssignedSeatTotal <= seatMinimum && newlyAssignedSeatTotal > seatMinimum) { - if (!currentContext.ProviderProviderAdmin(provider.Id)) - { - logger.LogError("Service user for provider ({ProviderID}) cannot scale a provider's seat count over the seat minimum", provider.Id); - - throw new BillingException(); - } - await update( seatMinimum, newlyAssignedSeatTotal); @@ -291,6 +269,26 @@ public class ProviderBillingService( } } + public async Task SeatAdjustmentResultsInPurchase( + Provider provider, + PlanType planType, + int seatAdjustment) + { + var providerPlan = await GetProviderPlanAsync(provider, planType); + + var seatMinimum = providerPlan.SeatMinimum; + + var currentlyAssignedSeatTotal = await GetAssignedSeatTotalAsync(provider, planType); + + var newlyAssignedSeatTotal = currentlyAssignedSeatTotal + seatAdjustment; + + return + // Below the limit to above the limit + (currentlyAssignedSeatTotal <= seatMinimum && newlyAssignedSeatTotal > seatMinimum) || + // Above the limit to further above the limit + (currentlyAssignedSeatTotal > seatMinimum && newlyAssignedSeatTotal > seatMinimum && newlyAssignedSeatTotal > currentlyAssignedSeatTotal); + } + public async Task SetupCustomer( Provider provider, TaxInfo taxInfo) @@ -431,75 +429,6 @@ public class ProviderBillingService( } } - public async Task ChangePlan(ChangeProviderPlanCommand command) - { - var plan = await providerPlanRepository.GetByIdAsync(command.ProviderPlanId); - - if (plan == null) - { - throw new BadRequestException("Provider plan not found."); - } - - if (plan.PlanType == command.NewPlan) - { - return; - } - - var oldPlanConfiguration = StaticStore.GetPlan(plan.PlanType); - - plan.PlanType = command.NewPlan; - await providerPlanRepository.ReplaceAsync(plan); - - Subscription subscription; - try - { - subscription = await stripeAdapter.ProviderSubscriptionGetAsync(command.GatewaySubscriptionId, plan.ProviderId); - } - catch (InvalidOperationException) - { - throw new ConflictException("Subscription not found."); - } - - var oldSubscriptionItem = subscription.Items.SingleOrDefault(x => - x.Price.Id == oldPlanConfiguration.PasswordManager.StripeProviderPortalSeatPlanId); - - var updateOptions = new SubscriptionUpdateOptions - { - Items = - [ - new SubscriptionItemOptions - { - Price = StaticStore.GetPlan(command.NewPlan).PasswordManager.StripeProviderPortalSeatPlanId, - Quantity = oldSubscriptionItem!.Quantity - }, - new SubscriptionItemOptions - { - Id = oldSubscriptionItem.Id, - Deleted = true - } - ] - }; - - await stripeAdapter.SubscriptionUpdateAsync(command.GatewaySubscriptionId, updateOptions); - - // Refactor later to ?ChangeClientPlanCommand? (ProviderPlanId, ProviderId, OrganizationId) - // 1. Retrieve PlanType and PlanName for ProviderPlan - // 2. Assign PlanType & PlanName to Organization - var providerOrganizations = await providerOrganizationRepository.GetManyDetailsByProviderAsync(plan.ProviderId); - - foreach (var providerOrganization in providerOrganizations) - { - var organization = await organizationRepository.GetByIdAsync(providerOrganization.OrganizationId); - if (organization == null) - { - throw new ConflictException($"Organization '{providerOrganization.Id}' not found."); - } - organization.PlanType = command.NewPlan; - organization.Plan = StaticStore.GetPlan(command.NewPlan).Name; - await organizationRepository.ReplaceAsync(organization); - } - } - public async Task UpdateSeatMinimums(UpdateProviderSeatMinimumsCommand command) { if (command.Configuration.Any(x => x.SeatsMinimum < 0)) @@ -610,4 +539,32 @@ public class ProviderBillingService( await providerPlanRepository.ReplaceAsync(providerPlan); }; + + // TODO: Replace with SPROC + private async Task GetAssignedSeatTotalAsync(Provider provider, PlanType planType) + { + var providerOrganizations = + await providerOrganizationRepository.GetManyDetailsByProviderAsync(provider.Id); + + var plan = StaticStore.GetPlan(planType); + + return providerOrganizations + .Where(providerOrganization => providerOrganization.Plan == plan.Name && providerOrganization.Status == OrganizationStatusType.Managed) + .Sum(providerOrganization => providerOrganization.Seats ?? 0); + } + + // TODO: Replace with SPROC + private async Task GetProviderPlanAsync(Provider provider, PlanType planType) + { + var providerPlans = await providerPlanRepository.GetByProviderId(provider.Id); + + var providerPlan = providerPlans.FirstOrDefault(x => x.PlanType == planType); + + if (providerPlan == null || !providerPlan.IsConfigured()) + { + throw new BillingException(message: "Provider plan is missing or misconfigured"); + } + + return providerPlan; + } } diff --git a/bitwarden_license/test/Commercial.Core.Test/Billing/ProviderBillingServiceTests.cs b/bitwarden_license/test/Commercial.Core.Test/Billing/ProviderBillingServiceTests.cs index 7c3e8cad8..881a98455 100644 --- a/bitwarden_license/test/Commercial.Core.Test/Billing/ProviderBillingServiceTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/Billing/ProviderBillingServiceTests.cs @@ -4,17 +4,14 @@ using Bit.Commercial.Core.Billing; using Bit.Commercial.Core.Billing.Models; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; -using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Models.Data.Provider; using Bit.Core.AdminConsole.Repositories; -using Bit.Core.Billing; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Entities; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Repositories; using Bit.Core.Billing.Services; using Bit.Core.Billing.Services.Contracts; -using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; @@ -36,460 +33,151 @@ namespace Bit.Commercial.Core.Test.Billing; [SutProviderCustomize] public class ProviderBillingServiceTests { - #region AssignSeatsToClientOrganization & ScaleSeats + #region ChangePlan [Theory, BitAutoData] - public Task AssignSeatsToClientOrganization_NullProvider_ArgumentNullException( - Organization organization, - int seats, - SutProvider sutProvider) - => Assert.ThrowsAsync(() => - sutProvider.Sut.AssignSeatsToClientOrganization(null, organization, seats)); - - [Theory, BitAutoData] - public Task AssignSeatsToClientOrganization_NullOrganization_ArgumentNullException( - Provider provider, - int seats, - SutProvider sutProvider) - => Assert.ThrowsAsync(() => - sutProvider.Sut.AssignSeatsToClientOrganization(provider, null, seats)); - - [Theory, BitAutoData] - public Task AssignSeatsToClientOrganization_NegativeSeats_BillingException( - Provider provider, - Organization organization, - SutProvider sutProvider) - => Assert.ThrowsAsync(() => - sutProvider.Sut.AssignSeatsToClientOrganization(provider, organization, -5)); - - [Theory, BitAutoData] - public async Task AssignSeatsToClientOrganization_CurrentSeatsMatchesNewSeats_NoOp( - Provider provider, - Organization organization, - int seats, + public async Task ChangePlan_NullProviderPlan_ThrowsBadRequestException( + ChangeProviderPlanCommand command, SutProvider sutProvider) { - organization.PlanType = PlanType.TeamsMonthly; + // Arrange + var providerPlanRepository = sutProvider.GetDependency(); + providerPlanRepository.GetByIdAsync(Arg.Any()).Returns((ProviderPlan)null); - organization.Seats = seats; + // Act + var actual = await Assert.ThrowsAsync(() => sutProvider.Sut.ChangePlan(command)); - await sutProvider.Sut.AssignSeatsToClientOrganization(provider, organization, seats); - - await sutProvider.GetDependency().DidNotReceive().GetByProviderId(provider.Id); + // Assert + Assert.Equal("Provider plan not found.", actual.Message); } [Theory, BitAutoData] - public async Task - AssignSeatsToClientOrganization_OrganizationPlanTypeDoesNotSupportConsolidatedBilling_ContactSupport( - Provider provider, - Organization organization, - int seats, - SutProvider sutProvider) - { - organization.PlanType = PlanType.FamiliesAnnually; - - await ThrowsBillingExceptionAsync(() => - sutProvider.Sut.AssignSeatsToClientOrganization(provider, organization, seats)); - } - - [Theory, BitAutoData] - public async Task AssignSeatsToClientOrganization_ProviderPlanIsNotConfigured_ContactSupport( - Provider provider, - Organization organization, - int seats, + public async Task ChangePlan_ProviderNotFound_DoesNothing( + ChangeProviderPlanCommand command, SutProvider sutProvider) { - organization.PlanType = PlanType.TeamsMonthly; - - sutProvider.GetDependency().GetByProviderId(provider.Id).Returns(new List + // Arrange + var providerPlanRepository = sutProvider.GetDependency(); + var stripeAdapter = sutProvider.GetDependency(); + var existingPlan = new ProviderPlan { - new() { Id = Guid.NewGuid(), PlanType = PlanType.TeamsMonthly, ProviderId = provider.Id } - }); - - await ThrowsBillingExceptionAsync(() => - sutProvider.Sut.AssignSeatsToClientOrganization(provider, organization, seats)); - } - - [Theory, BitAutoData] - public async Task AssignSeatsToClientOrganization_BelowToBelow_Succeeds( - Provider provider, - Organization organization, - SutProvider sutProvider) - { - organization.Seats = 10; - - organization.PlanType = PlanType.TeamsMonthly; - - // Scale up 10 seats - const int seats = 20; - - var providerPlans = new List - { - new() - { - Id = Guid.NewGuid(), - PlanType = PlanType.TeamsMonthly, - ProviderId = provider.Id, - PurchasedSeats = 0, - // 100 minimum - SeatMinimum = 100, - AllocatedSeats = 50 - }, - new() - { - Id = Guid.NewGuid(), - PlanType = PlanType.EnterpriseMonthly, - ProviderId = provider.Id, - PurchasedSeats = 0, - SeatMinimum = 500, - AllocatedSeats = 0 - } + Id = command.ProviderPlanId, + PlanType = command.NewPlan, + PurchasedSeats = 0, + AllocatedSeats = 0, + SeatMinimum = 0 }; + providerPlanRepository + .GetByIdAsync(Arg.Is(p => p == command.ProviderPlanId)) + .Returns(existingPlan); - var providerPlan = providerPlans.First(); + // Act + await sutProvider.Sut.ChangePlan(command); - sutProvider.GetDependency().GetByProviderId(provider.Id).Returns(providerPlans); - - // 50 seats currently assigned with a seat minimum of 100 - sutProvider.GetDependency().GetByIdAsync(provider.Id).Returns(provider); - - var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly); - - sutProvider.GetDependency().GetManyDetailsByProviderAsync(provider.Id).Returns( - [ - new ProviderOrganizationOrganizationDetails - { - Plan = teamsMonthlyPlan.Name, - Status = OrganizationStatusType.Managed, - Seats = 25 - }, - new ProviderOrganizationOrganizationDetails - { - Plan = teamsMonthlyPlan.Name, - Status = OrganizationStatusType.Managed, - Seats = 25 - } - ]); - - await sutProvider.Sut.AssignSeatsToClientOrganization(provider, organization, seats); - - // 50 assigned seats + 10 seat scale up = 60 seats, well below the 100 minimum - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().AdjustSeats( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()); - - await sutProvider.GetDependency().Received(1).ReplaceAsync(Arg.Is( - org => org.Id == organization.Id && org.Seats == seats)); - - await sutProvider.GetDependency().Received(1).ReplaceAsync(Arg.Is( - pPlan => pPlan.AllocatedSeats == 60)); + // Assert + await providerPlanRepository.Received(0).ReplaceAsync(Arg.Any()); + await stripeAdapter.Received(0).SubscriptionUpdateAsync(Arg.Any(), Arg.Any()); } [Theory, BitAutoData] - public async Task AssignSeatsToClientOrganization_BelowToAbove_NotProviderAdmin_ContactSupport( - Provider provider, - Organization organization, + public async Task ChangePlan_SameProviderPlan_DoesNothing( + ChangeProviderPlanCommand command, SutProvider sutProvider) { - organization.Seats = 10; - - organization.PlanType = PlanType.TeamsMonthly; - - // Scale up 10 seats - const int seats = 20; - - var providerPlans = new List + // Arrange + var providerPlanRepository = sutProvider.GetDependency(); + var stripeAdapter = sutProvider.GetDependency(); + var existingPlan = new ProviderPlan { - new() - { - Id = Guid.NewGuid(), - PlanType = PlanType.TeamsMonthly, - ProviderId = provider.Id, - PurchasedSeats = 0, - // 100 minimum - SeatMinimum = 100, - AllocatedSeats = 95 - }, - new() - { - Id = Guid.NewGuid(), - PlanType = PlanType.EnterpriseMonthly, - ProviderId = provider.Id, - PurchasedSeats = 0, - SeatMinimum = 500, - AllocatedSeats = 0 - } + Id = command.ProviderPlanId, + PlanType = command.NewPlan, + PurchasedSeats = 0, + AllocatedSeats = 0, + SeatMinimum = 0 }; + providerPlanRepository + .GetByIdAsync(Arg.Is(p => p == command.ProviderPlanId)) + .Returns(existingPlan); - sutProvider.GetDependency().GetByProviderId(provider.Id).Returns(providerPlans); + // Act + await sutProvider.Sut.ChangePlan(command); - // 95 seats currently assigned with a seat minimum of 100 - sutProvider.GetDependency().GetByIdAsync(provider.Id).Returns(provider); - - var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly); - - sutProvider.GetDependency().GetManyDetailsByProviderAsync(provider.Id).Returns( - [ - new ProviderOrganizationOrganizationDetails - { - Plan = teamsMonthlyPlan.Name, - Status = OrganizationStatusType.Managed, - Seats = 60 - }, - new ProviderOrganizationOrganizationDetails - { - Plan = teamsMonthlyPlan.Name, - Status = OrganizationStatusType.Managed, - Seats = 35 - } - ]); - - sutProvider.GetDependency().ProviderProviderAdmin(provider.Id).Returns(false); - - await ThrowsBillingExceptionAsync(() => - sutProvider.Sut.AssignSeatsToClientOrganization(provider, organization, seats)); + // Assert + await providerPlanRepository.Received(0).ReplaceAsync(Arg.Any()); + await stripeAdapter.Received(0).SubscriptionUpdateAsync(Arg.Any(), Arg.Any()); } [Theory, BitAutoData] - public async Task AssignSeatsToClientOrganization_BelowToAbove_Succeeds( + public async Task ChangePlan_UpdatesSubscriptionCorrectly( + Guid providerPlanId, Provider provider, - Organization organization, SutProvider sutProvider) { - organization.Seats = 10; - - organization.PlanType = PlanType.TeamsMonthly; - - // Scale up 10 seats - const int seats = 20; - - var providerPlans = new List + // Arrange + var providerPlanRepository = sutProvider.GetDependency(); + var existingPlan = new ProviderPlan { - new() - { - Id = Guid.NewGuid(), - PlanType = PlanType.TeamsMonthly, - ProviderId = provider.Id, - PurchasedSeats = 0, - // 100 minimum - SeatMinimum = 100, - AllocatedSeats = 95 - }, - new() - { - Id = Guid.NewGuid(), - PlanType = PlanType.EnterpriseMonthly, - ProviderId = provider.Id, - PurchasedSeats = 0, - SeatMinimum = 500, - AllocatedSeats = 0 - } + Id = providerPlanId, + ProviderId = provider.Id, + PlanType = PlanType.EnterpriseAnnually, + PurchasedSeats = 2, + AllocatedSeats = 10, + SeatMinimum = 8 }; + providerPlanRepository + .GetByIdAsync(Arg.Is(p => p == providerPlanId)) + .Returns(existingPlan); - var providerPlan = providerPlans.First(); - - sutProvider.GetDependency().GetByProviderId(provider.Id).Returns(providerPlans); - - // 95 seats currently assigned with a seat minimum of 100 - sutProvider.GetDependency().GetByIdAsync(provider.Id).Returns(provider); - - var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly); - - sutProvider.GetDependency().GetManyDetailsByProviderAsync(provider.Id).Returns( - [ - new ProviderOrganizationOrganizationDetails + var stripeAdapter = sutProvider.GetDependency(); + stripeAdapter.ProviderSubscriptionGetAsync( + Arg.Is(provider.GatewaySubscriptionId), + Arg.Is(provider.Id)) + .Returns(new Subscription { - Plan = teamsMonthlyPlan.Name, - Status = OrganizationStatusType.Managed, - Seats = 60 - }, - new ProviderOrganizationOrganizationDetails - { - Plan = teamsMonthlyPlan.Name, - Status = OrganizationStatusType.Managed, - Seats = 35 - } - ]); + Id = provider.GatewaySubscriptionId, + Items = new StripeList + { + Data = + [ + new SubscriptionItem + { + Id = "si_ent_annual", + Price = new Price + { + Id = StaticStore.GetPlan(PlanType.EnterpriseAnnually).PasswordManager + .StripeProviderPortalSeatPlanId + }, + Quantity = 10 + } + ] + } + }); - sutProvider.GetDependency().ProviderProviderAdmin(provider.Id).Returns(true); + var command = + new ChangeProviderPlanCommand(providerPlanId, PlanType.EnterpriseMonthly, provider.GatewaySubscriptionId); - await sutProvider.Sut.AssignSeatsToClientOrganization(provider, organization, seats); + // Act + await sutProvider.Sut.ChangePlan(command); - // 95 current + 10 seat scale = 105 seats, 5 above the minimum - await sutProvider.GetDependency().Received(1).AdjustSeats( - provider, - StaticStore.GetPlan(providerPlan.PlanType), - providerPlan.SeatMinimum!.Value, - 105); + // Assert + await providerPlanRepository.Received(1) + .ReplaceAsync(Arg.Is(p => p.PlanType == PlanType.EnterpriseMonthly)); - await sutProvider.GetDependency().Received(1).ReplaceAsync(Arg.Is( - org => org.Id == organization.Id && org.Seats == seats)); + await stripeAdapter.Received(1) + .SubscriptionUpdateAsync( + Arg.Is(provider.GatewaySubscriptionId), + Arg.Is(p => + p.Items.Count(si => si.Id == "si_ent_annual" && si.Deleted == true) == 1)); - // 105 total seats - 100 minimum = 5 purchased seats - await sutProvider.GetDependency().Received(1).ReplaceAsync(Arg.Is( - pPlan => pPlan.Id == providerPlan.Id && pPlan.PurchasedSeats == 5 && pPlan.AllocatedSeats == 105)); - } - - [Theory, BitAutoData] - public async Task AssignSeatsToClientOrganization_AboveToAbove_Succeeds( - Provider provider, - Organization organization, - SutProvider sutProvider) - { - provider.Type = ProviderType.Msp; - - organization.Seats = 10; - - organization.PlanType = PlanType.TeamsMonthly; - - // Scale up 10 seats - const int seats = 20; - - var providerPlans = new List - { - new() - { - Id = Guid.NewGuid(), - PlanType = PlanType.TeamsMonthly, - ProviderId = provider.Id, - // 10 additional purchased seats - PurchasedSeats = 10, - // 100 seat minimum - SeatMinimum = 100, - AllocatedSeats = 110 - }, - new() - { - Id = Guid.NewGuid(), - PlanType = PlanType.EnterpriseMonthly, - ProviderId = provider.Id, - PurchasedSeats = 0, - SeatMinimum = 500, - AllocatedSeats = 0 - } - }; - - var providerPlan = providerPlans.First(); - - sutProvider.GetDependency().GetByProviderId(provider.Id).Returns(providerPlans); - - // 110 seats currently assigned with a seat minimum of 100 - sutProvider.GetDependency().GetByIdAsync(provider.Id).Returns(provider); - - var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly); - - sutProvider.GetDependency().GetManyDetailsByProviderAsync(provider.Id).Returns( - [ - new ProviderOrganizationOrganizationDetails - { - Plan = teamsMonthlyPlan.Name, - Status = OrganizationStatusType.Managed, - Seats = 60 - }, - new ProviderOrganizationOrganizationDetails - { - Plan = teamsMonthlyPlan.Name, - Status = OrganizationStatusType.Managed, - Seats = 50 - } - ]); - - await sutProvider.Sut.AssignSeatsToClientOrganization(provider, organization, seats); - - // 110 current + 10 seat scale up = 120 seats - await sutProvider.GetDependency().Received(1).AdjustSeats( - provider, - StaticStore.GetPlan(providerPlan.PlanType), - 110, - 120); - - await sutProvider.GetDependency().Received(1).ReplaceAsync(Arg.Is( - org => org.Id == organization.Id && org.Seats == seats)); - - // 120 total seats - 100 seat minimum = 20 purchased seats - await sutProvider.GetDependency().Received(1).ReplaceAsync(Arg.Is( - pPlan => pPlan.Id == providerPlan.Id && pPlan.PurchasedSeats == 20 && pPlan.AllocatedSeats == 120)); - } - - [Theory, BitAutoData] - public async Task AssignSeatsToClientOrganization_AboveToBelow_Succeeds( - Provider provider, - Organization organization, - SutProvider sutProvider) - { - organization.Seats = 50; - - organization.PlanType = PlanType.TeamsMonthly; - - // Scale down 30 seats - const int seats = 20; - - var providerPlans = new List - { - new() - { - Id = Guid.NewGuid(), - PlanType = PlanType.TeamsMonthly, - ProviderId = provider.Id, - // 10 additional purchased seats - PurchasedSeats = 10, - // 100 seat minimum - SeatMinimum = 100, - AllocatedSeats = 110 - }, - new() - { - Id = Guid.NewGuid(), - PlanType = PlanType.EnterpriseMonthly, - ProviderId = provider.Id, - PurchasedSeats = 0, - SeatMinimum = 500, - AllocatedSeats = 0 - } - }; - - var providerPlan = providerPlans.First(); - - sutProvider.GetDependency().GetByProviderId(provider.Id).Returns(providerPlans); - - // 110 seats currently assigned with a seat minimum of 100 - sutProvider.GetDependency().GetByIdAsync(provider.Id).Returns(provider); - - var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly); - - sutProvider.GetDependency().GetManyDetailsByProviderAsync(provider.Id).Returns( - [ - new ProviderOrganizationOrganizationDetails - { - Plan = teamsMonthlyPlan.Name, - Status = OrganizationStatusType.Managed, - Seats = 60 - }, - new ProviderOrganizationOrganizationDetails - { - Plan = teamsMonthlyPlan.Name, - Status = OrganizationStatusType.Managed, - Seats = 50 - } - ]); - - await sutProvider.Sut.AssignSeatsToClientOrganization(provider, organization, seats); - - // 110 seats - 30 scale down seats = 80 seats, below the 100 seat minimum. - await sutProvider.GetDependency().Received(1).AdjustSeats( - provider, - StaticStore.GetPlan(providerPlan.PlanType), - 110, - providerPlan.SeatMinimum!.Value); - - await sutProvider.GetDependency().Received(1).ReplaceAsync(Arg.Is( - org => org.Id == organization.Id && org.Seats == seats)); - - // Being below the seat minimum means no purchased seats. - await sutProvider.GetDependency().Received(1).ReplaceAsync(Arg.Is( - pPlan => pPlan.Id == providerPlan.Id && pPlan.PurchasedSeats == 0 && pPlan.AllocatedSeats == 80)); + var newPlanCfg = StaticStore.GetPlan(command.NewPlan); + await stripeAdapter.Received(1) + .SubscriptionUpdateAsync( + Arg.Is(provider.GatewaySubscriptionId), + Arg.Is(p => + p.Items.Count(si => + si.Price == newPlanCfg.PasswordManager.StripeProviderPortalSeatPlanId && + si.Deleted == default && + si.Quantity == 10) == 1)); } #endregion @@ -673,66 +361,335 @@ public class ProviderBillingServiceTests #endregion - #region GetAssignedSeatTotalForPlanOrThrow + #region ScaleSeats [Theory, BitAutoData] - public async Task GetAssignedSeatTotalForPlanOrThrow_NullProvider_ContactSupport( - Guid providerId, - SutProvider sutProvider) - => await ThrowsBillingExceptionAsync(() => - sutProvider.Sut.GetAssignedSeatTotalForPlanOrThrow(providerId, PlanType.TeamsMonthly)); - - [Theory, BitAutoData] - public async Task GetAssignedSeatTotalForPlanOrThrow_ResellerProvider_ContactSupport( - Guid providerId, + public async Task ScaleSeats_BelowToBelow_Succeeds( Provider provider, SutProvider sutProvider) { - provider.Type = ProviderType.Reseller; - - sutProvider.GetDependency().GetByIdAsync(providerId).Returns(provider); - - await ThrowsBillingExceptionAsync( - () => sutProvider.Sut.GetAssignedSeatTotalForPlanOrThrow(providerId, PlanType.TeamsMonthly)); - } - - [Theory, BitAutoData] - public async Task GetAssignedSeatTotalForPlanOrThrow_Succeeds( - Guid providerId, - Provider provider, - SutProvider sutProvider) - { - provider.Type = ProviderType.Msp; - - sutProvider.GetDependency().GetByIdAsync(providerId).Returns(provider); - - var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly); - var enterpriseMonthlyPlan = StaticStore.GetPlan(PlanType.EnterpriseMonthly); - - var providerOrganizationOrganizationDetailList = new List + var providerPlans = new List { - new() { Plan = teamsMonthlyPlan.Name, Status = OrganizationStatusType.Managed, Seats = 10 }, - new() { Plan = teamsMonthlyPlan.Name, Status = OrganizationStatusType.Managed, Seats = 10 }, new() { - // Ignored because of status. - Plan = teamsMonthlyPlan.Name, Status = OrganizationStatusType.Created, Seats = 100 + Id = Guid.NewGuid(), + PlanType = PlanType.TeamsMonthly, + ProviderId = provider.Id, + PurchasedSeats = 0, + SeatMinimum = 100, + AllocatedSeats = 50 }, new() { - // Ignored because of plan. - Plan = enterpriseMonthlyPlan.Name, Status = OrganizationStatusType.Managed, Seats = 30 + Id = Guid.NewGuid(), + PlanType = PlanType.EnterpriseMonthly, + ProviderId = provider.Id, + PurchasedSeats = 0, + SeatMinimum = 500, + AllocatedSeats = 0 } }; - sutProvider.GetDependency() - .GetManyDetailsByProviderAsync(providerId) - .Returns(providerOrganizationOrganizationDetailList); + sutProvider.GetDependency().GetByProviderId(provider.Id).Returns(providerPlans); - var assignedSeatTotal = - await sutProvider.Sut.GetAssignedSeatTotalForPlanOrThrow(providerId, PlanType.TeamsMonthly); + // 50 seats currently assigned with a seat minimum of 100 + var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly); - Assert.Equal(20, assignedSeatTotal); + sutProvider.GetDependency().GetManyDetailsByProviderAsync(provider.Id).Returns( + [ + new ProviderOrganizationOrganizationDetails + { + Plan = teamsMonthlyPlan.Name, + Status = OrganizationStatusType.Managed, + Seats = 25 + }, + new ProviderOrganizationOrganizationDetails + { + Plan = teamsMonthlyPlan.Name, + Status = OrganizationStatusType.Managed, + Seats = 25 + } + ]); + + await sutProvider.Sut.ScaleSeats(provider, PlanType.TeamsMonthly, 10); + + // 50 assigned seats + 10 seat scale up = 60 seats, well below the 100 minimum + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().AdjustSeats( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()); + + await sutProvider.GetDependency().Received(1).ReplaceAsync(Arg.Is( + pPlan => pPlan.AllocatedSeats == 60)); + } + + [Theory, BitAutoData] + public async Task ScaleSeats_BelowToAbove_Succeeds( + Provider provider, + SutProvider sutProvider) + { + var providerPlans = new List + { + new() + { + Id = Guid.NewGuid(), + PlanType = PlanType.TeamsMonthly, + ProviderId = provider.Id, + PurchasedSeats = 0, + SeatMinimum = 100, + AllocatedSeats = 95 + }, + new() + { + Id = Guid.NewGuid(), + PlanType = PlanType.EnterpriseMonthly, + ProviderId = provider.Id, + PurchasedSeats = 0, + SeatMinimum = 500, + AllocatedSeats = 0 + } + }; + + var providerPlan = providerPlans.First(); + + sutProvider.GetDependency().GetByProviderId(provider.Id).Returns(providerPlans); + + // 95 seats currently assigned with a seat minimum of 100 + var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly); + + sutProvider.GetDependency().GetManyDetailsByProviderAsync(provider.Id).Returns( + [ + new ProviderOrganizationOrganizationDetails + { + Plan = teamsMonthlyPlan.Name, + Status = OrganizationStatusType.Managed, + Seats = 60 + }, + new ProviderOrganizationOrganizationDetails + { + Plan = teamsMonthlyPlan.Name, + Status = OrganizationStatusType.Managed, + Seats = 35 + } + ]); + + await sutProvider.Sut.ScaleSeats(provider, PlanType.TeamsMonthly, 10); + + // 95 current + 10 seat scale = 105 seats, 5 above the minimum + await sutProvider.GetDependency().Received(1).AdjustSeats( + provider, + StaticStore.GetPlan(providerPlan.PlanType), + providerPlan.SeatMinimum!.Value, + 105); + + // 105 total seats - 100 minimum = 5 purchased seats + await sutProvider.GetDependency().Received(1).ReplaceAsync(Arg.Is( + pPlan => pPlan.Id == providerPlan.Id && pPlan.PurchasedSeats == 5 && pPlan.AllocatedSeats == 105)); + } + + [Theory, BitAutoData] + public async Task ScaleSeats_AboveToAbove_Succeeds( + Provider provider, + SutProvider sutProvider) + { + var providerPlans = new List + { + new() + { + Id = Guid.NewGuid(), + PlanType = PlanType.TeamsMonthly, + ProviderId = provider.Id, + PurchasedSeats = 10, + SeatMinimum = 100, + AllocatedSeats = 110 + }, + new() + { + Id = Guid.NewGuid(), + PlanType = PlanType.EnterpriseMonthly, + ProviderId = provider.Id, + PurchasedSeats = 0, + SeatMinimum = 500, + AllocatedSeats = 0 + } + }; + + var providerPlan = providerPlans.First(); + + sutProvider.GetDependency().GetByProviderId(provider.Id).Returns(providerPlans); + + // 110 seats currently assigned with a seat minimum of 100 + var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly); + + sutProvider.GetDependency().GetManyDetailsByProviderAsync(provider.Id).Returns( + [ + new ProviderOrganizationOrganizationDetails + { + Plan = teamsMonthlyPlan.Name, + Status = OrganizationStatusType.Managed, + Seats = 60 + }, + new ProviderOrganizationOrganizationDetails + { + Plan = teamsMonthlyPlan.Name, + Status = OrganizationStatusType.Managed, + Seats = 50 + } + ]); + + await sutProvider.Sut.ScaleSeats(provider, PlanType.TeamsMonthly, 10); + + // 110 current + 10 seat scale up = 120 seats + await sutProvider.GetDependency().Received(1).AdjustSeats( + provider, + StaticStore.GetPlan(providerPlan.PlanType), + 110, + 120); + + // 120 total seats - 100 seat minimum = 20 purchased seats + await sutProvider.GetDependency().Received(1).ReplaceAsync(Arg.Is( + pPlan => pPlan.Id == providerPlan.Id && pPlan.PurchasedSeats == 20 && pPlan.AllocatedSeats == 120)); + } + + [Theory, BitAutoData] + public async Task ScaleSeats_AboveToBelow_Succeeds( + Provider provider, + SutProvider sutProvider) + { + var providerPlans = new List + { + new() + { + Id = Guid.NewGuid(), + PlanType = PlanType.TeamsMonthly, + ProviderId = provider.Id, + PurchasedSeats = 10, + SeatMinimum = 100, + AllocatedSeats = 110 + }, + new() + { + Id = Guid.NewGuid(), + PlanType = PlanType.EnterpriseMonthly, + ProviderId = provider.Id, + PurchasedSeats = 0, + SeatMinimum = 500, + AllocatedSeats = 0 + } + }; + + var providerPlan = providerPlans.First(); + + sutProvider.GetDependency().GetByProviderId(provider.Id).Returns(providerPlans); + + // 110 seats currently assigned with a seat minimum of 100 + var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly); + + sutProvider.GetDependency().GetManyDetailsByProviderAsync(provider.Id).Returns( + [ + new ProviderOrganizationOrganizationDetails + { + Plan = teamsMonthlyPlan.Name, + Status = OrganizationStatusType.Managed, + Seats = 60 + }, + new ProviderOrganizationOrganizationDetails + { + Plan = teamsMonthlyPlan.Name, + Status = OrganizationStatusType.Managed, + Seats = 50 + } + ]); + + await sutProvider.Sut.ScaleSeats(provider, PlanType.TeamsMonthly, -30); + + // 110 seats - 30 scale down seats = 80 seats, below the 100 seat minimum. + await sutProvider.GetDependency().Received(1).AdjustSeats( + provider, + StaticStore.GetPlan(providerPlan.PlanType), + 110, + providerPlan.SeatMinimum!.Value); + + // Being below the seat minimum means no purchased seats. + await sutProvider.GetDependency().Received(1).ReplaceAsync(Arg.Is( + pPlan => pPlan.Id == providerPlan.Id && pPlan.PurchasedSeats == 0 && pPlan.AllocatedSeats == 80)); + } + + #endregion + + #region SeatAdjustmentResultsInPurchase + + [Theory, BitAutoData] + public async Task SeatAdjustmentResultsInPurchase_BelowToAbove_True( + Provider provider, + PlanType planType, + SutProvider sutProvider) + { + sutProvider.GetDependency().GetByProviderId(provider.Id).Returns([ + new ProviderPlan + { + PlanType = planType, + SeatMinimum = 10, + AllocatedSeats = 0, + PurchasedSeats = 0 + } + ]); + + sutProvider.GetDependency().GetManyDetailsByProviderAsync(provider.Id).Returns( + [ + new ProviderOrganizationOrganizationDetails + { + Plan = StaticStore.GetPlan(planType).Name, + Status = OrganizationStatusType.Managed, + Seats = 5 + } + ]); + + const int seatAdjustment = 10; + + var result = await sutProvider.Sut.SeatAdjustmentResultsInPurchase( + provider, + planType, + seatAdjustment); + + Assert.True(result); + } + + [Theory, BitAutoData] + public async Task SeatAdjustmentResultsInPurchase_AboveToFurtherAbove_True( + Provider provider, + PlanType planType, + SutProvider sutProvider) + { + sutProvider.GetDependency().GetByProviderId(provider.Id).Returns([ + new ProviderPlan + { + PlanType = planType, + SeatMinimum = 10, + AllocatedSeats = 0, + PurchasedSeats = 5 + } + ]); + + sutProvider.GetDependency().GetManyDetailsByProviderAsync(provider.Id).Returns( + [ + new ProviderOrganizationOrganizationDetails + { + Plan = StaticStore.GetPlan(planType).Name, + Status = OrganizationStatusType.Managed, + Seats = 15 + } + ]); + + const int seatAdjustment = 5; + + var result = await sutProvider.Sut.SeatAdjustmentResultsInPurchase( + provider, + planType, + seatAdjustment); + + Assert.True(result); } #endregion @@ -1012,158 +969,6 @@ public class ProviderBillingServiceTests #endregion - #region ChangePlan - - [Theory, BitAutoData] - public async Task ChangePlan_NullProviderPlan_ThrowsBadRequestException( - ChangeProviderPlanCommand command, - SutProvider sutProvider) - { - // Arrange - var providerPlanRepository = sutProvider.GetDependency(); - providerPlanRepository.GetByIdAsync(Arg.Any()).Returns((ProviderPlan)null); - - // Act - var actual = await Assert.ThrowsAsync(() => sutProvider.Sut.ChangePlan(command)); - - // Assert - Assert.Equal("Provider plan not found.", actual.Message); - } - - [Theory, BitAutoData] - public async Task ChangePlan_ProviderNotFound_DoesNothing( - ChangeProviderPlanCommand command, - SutProvider sutProvider) - { - // Arrange - var providerPlanRepository = sutProvider.GetDependency(); - var stripeAdapter = sutProvider.GetDependency(); - var existingPlan = new ProviderPlan - { - Id = command.ProviderPlanId, - PlanType = command.NewPlan, - PurchasedSeats = 0, - AllocatedSeats = 0, - SeatMinimum = 0 - }; - providerPlanRepository - .GetByIdAsync(Arg.Is(p => p == command.ProviderPlanId)) - .Returns(existingPlan); - - // Act - await sutProvider.Sut.ChangePlan(command); - - // Assert - await providerPlanRepository.Received(0).ReplaceAsync(Arg.Any()); - await stripeAdapter.Received(0).SubscriptionUpdateAsync(Arg.Any(), Arg.Any()); - } - - [Theory, BitAutoData] - public async Task ChangePlan_SameProviderPlan_DoesNothing( - ChangeProviderPlanCommand command, - SutProvider sutProvider) - { - // Arrange - var providerPlanRepository = sutProvider.GetDependency(); - var stripeAdapter = sutProvider.GetDependency(); - var existingPlan = new ProviderPlan - { - Id = command.ProviderPlanId, - PlanType = command.NewPlan, - PurchasedSeats = 0, - AllocatedSeats = 0, - SeatMinimum = 0 - }; - providerPlanRepository - .GetByIdAsync(Arg.Is(p => p == command.ProviderPlanId)) - .Returns(existingPlan); - - // Act - await sutProvider.Sut.ChangePlan(command); - - // Assert - await providerPlanRepository.Received(0).ReplaceAsync(Arg.Any()); - await stripeAdapter.Received(0).SubscriptionUpdateAsync(Arg.Any(), Arg.Any()); - } - - [Theory, BitAutoData] - public async Task ChangePlan_UpdatesSubscriptionCorrectly( - Guid providerPlanId, - Provider provider, - SutProvider sutProvider) - { - // Arrange - var providerPlanRepository = sutProvider.GetDependency(); - var existingPlan = new ProviderPlan - { - Id = providerPlanId, - ProviderId = provider.Id, - PlanType = PlanType.EnterpriseAnnually, - PurchasedSeats = 2, - AllocatedSeats = 10, - SeatMinimum = 8 - }; - providerPlanRepository - .GetByIdAsync(Arg.Is(p => p == providerPlanId)) - .Returns(existingPlan); - - var providerRepository = sutProvider.GetDependency(); - providerRepository.GetByIdAsync(Arg.Is(existingPlan.ProviderId)).Returns(provider); - - var stripeAdapter = sutProvider.GetDependency(); - stripeAdapter.ProviderSubscriptionGetAsync( - Arg.Is(provider.GatewaySubscriptionId), - Arg.Is(provider.Id)) - .Returns(new Subscription - { - Id = provider.GatewaySubscriptionId, - Items = new StripeList - { - Data = - [ - new SubscriptionItem - { - Id = "si_ent_annual", - Price = new Price - { - Id = StaticStore.GetPlan(PlanType.EnterpriseAnnually).PasswordManager - .StripeProviderPortalSeatPlanId - }, - Quantity = 10 - } - ] - } - }); - - var command = - new ChangeProviderPlanCommand(providerPlanId, PlanType.EnterpriseMonthly, provider.GatewaySubscriptionId); - - // Act - await sutProvider.Sut.ChangePlan(command); - - // Assert - await providerPlanRepository.Received(1) - .ReplaceAsync(Arg.Is(p => p.PlanType == PlanType.EnterpriseMonthly)); - - await stripeAdapter.Received(1) - .SubscriptionUpdateAsync( - Arg.Is(provider.GatewaySubscriptionId), - Arg.Is(p => - p.Items.Count(si => si.Id == "si_ent_annual" && si.Deleted == true) == 1)); - - var newPlanCfg = StaticStore.GetPlan(command.NewPlan); - await stripeAdapter.Received(1) - .SubscriptionUpdateAsync( - Arg.Is(provider.GatewaySubscriptionId), - Arg.Is(p => - p.Items.Count(si => - si.Price == newPlanCfg.PasswordManager.StripeProviderPortalSeatPlanId && - si.Deleted == default && - si.Quantity == 10) == 1)); - } - - #endregion - #region UpdateSeatMinimums [Theory, BitAutoData] @@ -1172,8 +977,6 @@ public class ProviderBillingServiceTests SutProvider sutProvider) { // Arrange - var providerRepository = sutProvider.GetDependency(); - providerRepository.GetByIdAsync(provider.Id).Returns(provider); var command = new UpdateProviderSeatMinimumsCommand( provider.Id, provider.GatewaySubscriptionId, @@ -1197,7 +1000,6 @@ public class ProviderBillingServiceTests // Arrange var stripeAdapter = sutProvider.GetDependency(); var providerPlanRepository = sutProvider.GetDependency(); - var providerRepository = sutProvider.GetDependency(); const string enterpriseLineItemId = "enterprise_line_item_id"; const string teamsLineItemId = "teams_line_item_id"; @@ -1235,7 +1037,6 @@ public class ProviderBillingServiceTests new() { PlanType = PlanType.TeamsMonthly, SeatMinimum = 30, PurchasedSeats = 0, AllocatedSeats = 25 } }; - providerRepository.GetByIdAsync(provider.Id).Returns(provider); providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans); var command = new UpdateProviderSeatMinimumsCommand( @@ -1274,8 +1075,6 @@ public class ProviderBillingServiceTests // Arrange var stripeAdapter = sutProvider.GetDependency(); var providerPlanRepository = sutProvider.GetDependency(); - var providerRepository = sutProvider.GetDependency(); - providerRepository.GetByIdAsync(provider.Id).Returns(provider); const string enterpriseLineItemId = "enterprise_line_item_id"; const string teamsLineItemId = "teams_line_item_id"; @@ -1349,8 +1148,6 @@ public class ProviderBillingServiceTests // Arrange var stripeAdapter = sutProvider.GetDependency(); var providerPlanRepository = sutProvider.GetDependency(); - var providerRepository = sutProvider.GetDependency(); - providerRepository.GetByIdAsync(provider.Id).Returns(provider); const string enterpriseLineItemId = "enterprise_line_item_id"; const string teamsLineItemId = "teams_line_item_id"; @@ -1418,8 +1215,6 @@ public class ProviderBillingServiceTests // Arrange var stripeAdapter = sutProvider.GetDependency(); var providerPlanRepository = sutProvider.GetDependency(); - var providerRepository = sutProvider.GetDependency(); - providerRepository.GetByIdAsync(provider.Id).Returns(provider); const string enterpriseLineItemId = "enterprise_line_item_id"; const string teamsLineItemId = "teams_line_item_id"; @@ -1493,8 +1288,6 @@ public class ProviderBillingServiceTests // Arrange var stripeAdapter = sutProvider.GetDependency(); var providerPlanRepository = sutProvider.GetDependency(); - var providerRepository = sutProvider.GetDependency(); - providerRepository.GetByIdAsync(provider.Id).Returns(provider); const string enterpriseLineItemId = "enterprise_line_item_id"; const string teamsLineItemId = "teams_line_item_id"; diff --git a/src/Admin/AdminConsole/Controllers/OrganizationsController.cs b/src/Admin/AdminConsole/Controllers/OrganizationsController.cs index efab8620c..db41e9282 100644 --- a/src/Admin/AdminConsole/Controllers/OrganizationsController.cs +++ b/src/Admin/AdminConsole/Controllers/OrganizationsController.cs @@ -5,8 +5,10 @@ using Bit.Admin.Services; using Bit.Admin.Utilities; using Bit.Core; using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Providers.Interfaces; using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Billing.Enums; using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Services; using Bit.Core.Context; @@ -230,7 +232,23 @@ public class OrganizationsController : Controller [SelfHosted(NotSelfHostedOnly = true)] public async Task Edit(Guid id, OrganizationEditModel model) { - var organization = await GetOrganization(id, model); + var organization = await _organizationRepository.GetByIdAsync(id); + + if (organization == null) + { + TempData["Error"] = "Could not find organization to update."; + return RedirectToAction("Index"); + } + + var existingOrganizationData = new Organization + { + Id = organization.Id, + Status = organization.Status, + PlanType = organization.PlanType, + Seats = organization.Seats + }; + + UpdateOrganization(organization, model); if (organization.UseSecretsManager && !StaticStore.GetPlan(organization.PlanType).SupportsSecretsManager) @@ -239,7 +257,12 @@ public class OrganizationsController : Controller return RedirectToAction("Edit", new { id }); } + await HandlePotentialProviderSeatScalingAsync( + existingOrganizationData, + model); + await _organizationRepository.ReplaceAsync(organization); + await _applicationCacheService.UpsertOrganizationAbilityAsync(organization); await _referenceEventService.RaiseEventAsync(new ReferenceEvent(ReferenceEventType.OrganizationEditedByAdmin, organization, _currentContext) { @@ -394,10 +417,9 @@ public class OrganizationsController : Controller return Json(null); } - private async Task GetOrganization(Guid id, OrganizationEditModel model) - { - var organization = await _organizationRepository.GetByIdAsync(id); + private void UpdateOrganization(Organization organization, OrganizationEditModel model) + { if (_accessControlService.UserHasPermission(Permission.Org_CheckEnabledBox)) { organization.Enabled = model.Enabled; @@ -449,7 +471,64 @@ public class OrganizationsController : Controller organization.GatewayCustomerId = model.GatewayCustomerId; organization.GatewaySubscriptionId = model.GatewaySubscriptionId; } + } - return organization; + private async Task HandlePotentialProviderSeatScalingAsync( + Organization organization, + OrganizationEditModel update) + { + var consolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling); + + var scaleMSPOnClientOrganizationUpdate = + _featureService.IsEnabled(FeatureFlagKeys.PM14401_ScaleMSPOnClientOrganizationUpdate); + + if (!consolidatedBillingEnabled || !scaleMSPOnClientOrganizationUpdate) + { + return; + } + + var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id); + + // No scaling required + if (provider is not { Type: ProviderType.Msp, Status: ProviderStatusType.Billable } || + organization is not { Status: OrganizationStatusType.Managed } || + !organization.Seats.HasValue || + update is { Seats: null, PlanType: null } || + update is { PlanType: not PlanType.TeamsMonthly and not PlanType.EnterpriseMonthly } || + (PlanTypesMatch() && SeatsMatch())) + { + return; + } + + // Only scale the plan + if (!PlanTypesMatch() && SeatsMatch()) + { + await _providerBillingService.ScaleSeats(provider, organization.PlanType, -organization.Seats.Value); + await _providerBillingService.ScaleSeats(provider, update.PlanType!.Value, organization.Seats.Value); + } + // Only scale the seats + else if (PlanTypesMatch() && !SeatsMatch()) + { + var seatAdjustment = update.Seats!.Value - organization.Seats.Value; + await _providerBillingService.ScaleSeats(provider, organization.PlanType, seatAdjustment); + } + // Scale both + else if (!PlanTypesMatch() && !SeatsMatch()) + { + var seatAdjustment = update.Seats!.Value - organization.Seats.Value; + var planTypeAdjustment = organization.Seats.Value; + var totalAdjustment = seatAdjustment + planTypeAdjustment; + + await _providerBillingService.ScaleSeats(provider, organization.PlanType, -organization.Seats.Value); + await _providerBillingService.ScaleSeats(provider, update.PlanType!.Value, totalAdjustment); + } + + return; + + bool PlanTypesMatch() + => update.PlanType.HasValue && update.PlanType.Value == organization.PlanType; + + bool SeatsMatch() + => update.Seats.HasValue && update.Seats.Value == organization.Seats; } } diff --git a/src/Api/Billing/Controllers/BaseBillingController.cs b/src/Api/Billing/Controllers/BaseBillingController.cs index 81b8b29f2..5f7005fdf 100644 --- a/src/Api/Billing/Controllers/BaseBillingController.cs +++ b/src/Api/Billing/Controllers/BaseBillingController.cs @@ -22,9 +22,9 @@ public abstract class BaseBillingController : Controller new ErrorResponseModel(message), statusCode: StatusCodes.Status500InternalServerError); - public static JsonHttpResult Unauthorized() => + public static JsonHttpResult Unauthorized(string message = "Unauthorized.") => TypedResults.Json( - new ErrorResponseModel("Unauthorized."), + new ErrorResponseModel(message), statusCode: StatusCodes.Status401Unauthorized); } } diff --git a/src/Api/Billing/Controllers/ProviderClientsController.cs b/src/Api/Billing/Controllers/ProviderClientsController.cs index 23a6da459..700dd4a2e 100644 --- a/src/Api/Billing/Controllers/ProviderClientsController.cs +++ b/src/Api/Billing/Controllers/ProviderClientsController.cs @@ -102,15 +102,27 @@ public class ProviderClientsController( var clientOrganization = await organizationRepository.GetByIdAsync(providerOrganization.OrganizationId); - if (clientOrganization.Seats != requestBody.AssignedSeats) + if (clientOrganization is not { Status: OrganizationStatusType.Managed }) { - await providerBillingService.AssignSeatsToClientOrganization( - provider, - clientOrganization, - requestBody.AssignedSeats); + return Error.ServerError(); } + var seatAdjustment = requestBody.AssignedSeats - (clientOrganization.Seats ?? 0); + + var seatAdjustmentResultsInPurchase = await providerBillingService.SeatAdjustmentResultsInPurchase( + provider, + clientOrganization.PlanType, + seatAdjustment); + + if (seatAdjustmentResultsInPurchase && !currentContext.ProviderProviderAdmin(provider.Id)) + { + return Error.Unauthorized("Service users cannot purchase additional seats."); + } + + await providerBillingService.ScaleSeats(provider, clientOrganization.PlanType, seatAdjustment); + clientOrganization.Name = requestBody.Name; + clientOrganization.Seats = requestBody.AssignedSeats; await organizationRepository.ReplaceAsync(clientOrganization); diff --git a/src/Core/Billing/Services/IProviderBillingService.cs b/src/Core/Billing/Services/IProviderBillingService.cs index e353e5515..20e740762 100644 --- a/src/Core/Billing/Services/IProviderBillingService.cs +++ b/src/Core/Billing/Services/IProviderBillingService.cs @@ -1,6 +1,5 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; -using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.Billing.Entities; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Services.Contracts; @@ -12,18 +11,10 @@ namespace Bit.Core.Billing.Services; public interface IProviderBillingService { /// - /// Assigns a specified number of to a client on behalf of - /// its . Seat adjustments for the client organization may autoscale the provider's Stripe - /// depending on the provider's seat minimum for the client 's - /// . + /// Changes the assigned provider plan for the provider. /// - /// The that manages the client . - /// The client whose you want to update. - /// The number of seats to assign to the client organization. - Task AssignSeatsToClientOrganization( - Provider provider, - Organization organization, - int seats); + /// The command to change the provider plan. + Task ChangePlan(ChangeProviderPlanCommand command); /// /// Create a Stripe for the provided client utilizing @@ -44,18 +35,6 @@ public interface IProviderBillingService Task GenerateClientInvoiceReport( string invoiceId); - /// - /// Retrieves the number of seats an MSP has assigned to its client organizations with a specified . - /// - /// The ID of the MSP to retrieve the assigned seat total for. - /// The type of plan to retrieve the assigned seat total for. - /// An representing the number of seats the provider has assigned to its client organizations with the specified . - /// Thrown when the provider represented by the is . - /// Thrown when the provider represented by the has . - Task GetAssignedSeatTotalForPlanOrThrow( - Guid providerId, - PlanType planType); - /// /// Scales the 's seats for the specified using the provided . /// This operation may autoscale the provider's Stripe depending on the 's seat minimum for the @@ -69,6 +48,22 @@ public interface IProviderBillingService PlanType planType, int seatAdjustment); + /// + /// Determines whether the provided will result in a purchase for the 's . + /// Seat adjustments that result in purchases include: + /// + /// The going from below the seat minimum to above the seat minimum for the provided + /// The going from above the seat minimum to further above the seat minimum for the provided + /// + /// + /// The provider to check seat adjustments for. + /// The plan type to check seat adjustments for. + /// The change in seats for the 's . + Task SeatAdjustmentResultsInPurchase( + Provider provider, + PlanType planType, + int seatAdjustment); + /// /// For use during the provider setup process, this method creates a Stripe for the specified utilizing the provided . /// @@ -90,12 +85,5 @@ public interface IProviderBillingService Task SetupSubscription( Provider provider); - /// - /// Changes the assigned provider plan for the provider. - /// - /// The command to change the provider plan. - /// - Task ChangePlan(ChangeProviderPlanCommand command); - Task UpdateSeatMinimums(UpdateProviderSeatMinimumsCommand command); } diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 67d463bac..5830b480e 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -156,6 +156,7 @@ public static class FeatureFlagKeys public const string NewDeviceVerificationTemporaryDismiss = "new-device-temporary-dismiss"; public const string NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss"; public const string SecurityTasks = "security-tasks"; + public const string PM14401_ScaleMSPOnClientOrganizationUpdate = "PM-14401-scale-msp-on-client-organization-update"; public static List GetAllKeys() { diff --git a/test/Admin.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs b/test/Admin.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs new file mode 100644 index 000000000..af98fef03 --- /dev/null +++ b/test/Admin.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs @@ -0,0 +1,343 @@ +using Bit.Admin.AdminConsole.Controllers; +using Bit.Admin.AdminConsole.Models; +using Bit.Core; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Entities.Provider; +using Bit.Core.AdminConsole.Enums.Provider; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Services; +using Bit.Core.Enums; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; + +namespace Admin.Test.AdminConsole.Controllers; + +[ControllerCustomize(typeof(OrganizationsController))] +[SutProviderCustomize] +public class OrganizationsControllerTests +{ + #region Edit (POST) + + [BitAutoData] + [SutProviderCustomize] + [Theory] + public async Task Edit_ProviderSeatScaling_RequiredFFDisabled_NoOp( + SutProvider sutProvider) + { + // Arrange + var organizationId = new Guid(); + var update = new OrganizationEditModel { UseSecretsManager = false }; + + var organization = new Organization + { + Id = organizationId + }; + + sutProvider.GetDependency().GetByIdAsync(organizationId) + .Returns(organization); + + // Act + _ = await sutProvider.Sut.Edit(organizationId, update); + + // Assert + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .ScaleSeats(Arg.Any(), Arg.Any(), Arg.Any()); + } + + [BitAutoData] + [SutProviderCustomize] + [Theory] + public async Task Edit_ProviderSeatScaling_NonBillableProvider_NoOp( + SutProvider sutProvider) + { + // Arrange + var organizationId = new Guid(); + var update = new OrganizationEditModel { UseSecretsManager = false }; + + var organization = new Organization + { + Id = organizationId + }; + + sutProvider.GetDependency().GetByIdAsync(organizationId) + .Returns(organization); + + var featureService = sutProvider.GetDependency(); + + featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true); + featureService.IsEnabled(FeatureFlagKeys.PM14401_ScaleMSPOnClientOrganizationUpdate).Returns(true); + + var provider = new Provider { Type = ProviderType.Msp, Status = ProviderStatusType.Created }; + + sutProvider.GetDependency().GetByOrganizationIdAsync(organizationId).Returns(provider); + + // Act + _ = await sutProvider.Sut.Edit(organizationId, update); + + // Assert + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .ScaleSeats(Arg.Any(), Arg.Any(), Arg.Any()); + } + + [BitAutoData] + [SutProviderCustomize] + [Theory] + public async Task Edit_ProviderSeatScaling_UnmanagedOrganization_NoOp( + SutProvider sutProvider) + { + // Arrange + var organizationId = new Guid(); + var update = new OrganizationEditModel { UseSecretsManager = false }; + + var organization = new Organization + { + Id = organizationId, + Status = OrganizationStatusType.Created + }; + + sutProvider.GetDependency().GetByIdAsync(organizationId) + .Returns(organization); + + var featureService = sutProvider.GetDependency(); + + featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true); + featureService.IsEnabled(FeatureFlagKeys.PM14401_ScaleMSPOnClientOrganizationUpdate).Returns(true); + + var provider = new Provider { Type = ProviderType.Msp, Status = ProviderStatusType.Billable }; + + sutProvider.GetDependency().GetByOrganizationIdAsync(organizationId).Returns(provider); + + // Act + _ = await sutProvider.Sut.Edit(organizationId, update); + + // Assert + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .ScaleSeats(Arg.Any(), Arg.Any(), Arg.Any()); + } + + [BitAutoData] + [SutProviderCustomize] + [Theory] + public async Task Edit_ProviderSeatScaling_NonCBPlanType_NoOp( + SutProvider sutProvider) + { + // Arrange + var organizationId = new Guid(); + + var update = new OrganizationEditModel + { + UseSecretsManager = false, + Seats = 10, + PlanType = PlanType.FamiliesAnnually + }; + + var organization = new Organization + { + Id = organizationId, + Status = OrganizationStatusType.Managed, + Seats = 10 + }; + + sutProvider.GetDependency().GetByIdAsync(organizationId) + .Returns(organization); + + var featureService = sutProvider.GetDependency(); + + featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true); + featureService.IsEnabled(FeatureFlagKeys.PM14401_ScaleMSPOnClientOrganizationUpdate).Returns(true); + + var provider = new Provider { Type = ProviderType.Msp, Status = ProviderStatusType.Billable }; + + sutProvider.GetDependency().GetByOrganizationIdAsync(organizationId).Returns(provider); + + // Act + _ = await sutProvider.Sut.Edit(organizationId, update); + + // Assert + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .ScaleSeats(Arg.Any(), Arg.Any(), Arg.Any()); + } + + [BitAutoData] + [SutProviderCustomize] + [Theory] + public async Task Edit_ProviderSeatScaling_NoUpdateRequired_NoOp( + SutProvider sutProvider) + { + // Arrange + var organizationId = new Guid(); + var update = new OrganizationEditModel + { + UseSecretsManager = false, + Seats = 10, + PlanType = PlanType.EnterpriseMonthly + }; + + var organization = new Organization + { + Id = organizationId, + Status = OrganizationStatusType.Managed, + Seats = 10, + PlanType = PlanType.EnterpriseMonthly + }; + + sutProvider.GetDependency().GetByIdAsync(organizationId) + .Returns(organization); + + var featureService = sutProvider.GetDependency(); + + featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true); + featureService.IsEnabled(FeatureFlagKeys.PM14401_ScaleMSPOnClientOrganizationUpdate).Returns(true); + + var provider = new Provider { Type = ProviderType.Msp, Status = ProviderStatusType.Billable }; + + sutProvider.GetDependency().GetByOrganizationIdAsync(organizationId).Returns(provider); + + // Act + _ = await sutProvider.Sut.Edit(organizationId, update); + + // Assert + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .ScaleSeats(Arg.Any(), Arg.Any(), Arg.Any()); + } + + [BitAutoData] + [SutProviderCustomize] + [Theory] + public async Task Edit_ProviderSeatScaling_PlanTypesUpdate_ScalesSeatsCorrectly( + SutProvider sutProvider) + { + // Arrange + var organizationId = new Guid(); + var update = new OrganizationEditModel + { + UseSecretsManager = false, + Seats = 10, + PlanType = PlanType.EnterpriseMonthly + }; + + var organization = new Organization + { + Id = organizationId, + Status = OrganizationStatusType.Managed, + Seats = 10, + PlanType = PlanType.TeamsMonthly + }; + + sutProvider.GetDependency().GetByIdAsync(organizationId) + .Returns(organization); + + var featureService = sutProvider.GetDependency(); + + featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true); + featureService.IsEnabled(FeatureFlagKeys.PM14401_ScaleMSPOnClientOrganizationUpdate).Returns(true); + + var provider = new Provider { Type = ProviderType.Msp, Status = ProviderStatusType.Billable }; + + sutProvider.GetDependency().GetByOrganizationIdAsync(organizationId).Returns(provider); + + // Act + _ = await sutProvider.Sut.Edit(organizationId, update); + + // Assert + var providerBillingService = sutProvider.GetDependency(); + + await providerBillingService.Received(1).ScaleSeats(provider, organization.PlanType, -organization.Seats.Value); + await providerBillingService.Received(1).ScaleSeats(provider, update.PlanType!.Value, organization.Seats.Value); + } + + [BitAutoData] + [SutProviderCustomize] + [Theory] + public async Task Edit_ProviderSeatScaling_SeatsUpdate_ScalesSeatsCorrectly( + SutProvider sutProvider) + { + // Arrange + var organizationId = new Guid(); + var update = new OrganizationEditModel + { + UseSecretsManager = false, + Seats = 15, + PlanType = PlanType.EnterpriseMonthly + }; + + var organization = new Organization + { + Id = organizationId, + Status = OrganizationStatusType.Managed, + Seats = 10, + PlanType = PlanType.EnterpriseMonthly + }; + + sutProvider.GetDependency().GetByIdAsync(organizationId) + .Returns(organization); + + var featureService = sutProvider.GetDependency(); + + featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true); + featureService.IsEnabled(FeatureFlagKeys.PM14401_ScaleMSPOnClientOrganizationUpdate).Returns(true); + + var provider = new Provider { Type = ProviderType.Msp, Status = ProviderStatusType.Billable }; + + sutProvider.GetDependency().GetByOrganizationIdAsync(organizationId).Returns(provider); + + // Act + _ = await sutProvider.Sut.Edit(organizationId, update); + + // Assert + var providerBillingService = sutProvider.GetDependency(); + + await providerBillingService.Received(1).ScaleSeats(provider, organization.PlanType, update.Seats!.Value - organization.Seats.Value); + } + + [BitAutoData] + [SutProviderCustomize] + [Theory] + public async Task Edit_ProviderSeatScaling_FullUpdate_ScalesSeatsCorrectly( + SutProvider sutProvider) + { + // Arrange + var organizationId = new Guid(); + var update = new OrganizationEditModel + { + UseSecretsManager = false, + Seats = 15, + PlanType = PlanType.EnterpriseMonthly + }; + + var organization = new Organization + { + Id = organizationId, + Status = OrganizationStatusType.Managed, + Seats = 10, + PlanType = PlanType.TeamsMonthly + }; + + sutProvider.GetDependency().GetByIdAsync(organizationId) + .Returns(organization); + + var featureService = sutProvider.GetDependency(); + + featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true); + featureService.IsEnabled(FeatureFlagKeys.PM14401_ScaleMSPOnClientOrganizationUpdate).Returns(true); + + var provider = new Provider { Type = ProviderType.Msp, Status = ProviderStatusType.Billable }; + + sutProvider.GetDependency().GetByOrganizationIdAsync(organizationId).Returns(provider); + + // Act + _ = await sutProvider.Sut.Edit(organizationId, update); + + // Assert + var providerBillingService = sutProvider.GetDependency(); + + await providerBillingService.Received(1).ScaleSeats(provider, organization.PlanType, -organization.Seats.Value); + await providerBillingService.Received(1).ScaleSeats(provider, update.PlanType!.Value, update.Seats!.Value - organization.Seats.Value + organization.Seats.Value); + } + + #endregion +} diff --git a/test/Api.Test/Billing/Controllers/ProviderClientsControllerTests.cs b/test/Api.Test/Billing/Controllers/ProviderClientsControllerTests.cs index 450ff9bf2..86bacd9aa 100644 --- a/test/Api.Test/Billing/Controllers/ProviderClientsControllerTests.cs +++ b/test/Api.Test/Billing/Controllers/ProviderClientsControllerTests.cs @@ -5,8 +5,11 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; +using Bit.Core.Billing.Enums; using Bit.Core.Billing.Services; +using Bit.Core.Context; using Bit.Core.Entities; +using Bit.Core.Enums; using Bit.Core.Models.Business; using Bit.Core.Repositories; using Bit.Core.Services; @@ -93,24 +96,7 @@ public class ProviderClientsControllerTests #region UpdateAsync [Theory, BitAutoData] - public async Task UpdateAsync_NoProviderOrganization_NotFound( - Provider provider, - Guid providerOrganizationId, - UpdateClientOrganizationRequestBody requestBody, - SutProvider sutProvider) - { - ConfigureStableProviderServiceUserInputs(provider, sutProvider); - - sutProvider.GetDependency().GetByIdAsync(providerOrganizationId) - .ReturnsNull(); - - var result = await sutProvider.Sut.UpdateAsync(provider.Id, providerOrganizationId, requestBody); - - AssertNotFound(result); - } - - [Theory, BitAutoData] - public async Task UpdateAsync_AssignedSeats_Ok( + public async Task UpdateAsync_ServiceUserMakingPurchase_Unauthorized( Provider provider, Guid providerOrganizationId, UpdateClientOrganizationRequestBody requestBody, @@ -118,6 +104,11 @@ public class ProviderClientsControllerTests Organization organization, SutProvider sutProvider) { + organization.PlanType = PlanType.TeamsMonthly; + organization.Seats = 10; + organization.Status = OrganizationStatusType.Managed; + requestBody.AssignedSeats = 20; + ConfigureStableProviderServiceUserInputs(provider, sutProvider); sutProvider.GetDependency().GetByIdAsync(providerOrganizationId) @@ -126,49 +117,57 @@ public class ProviderClientsControllerTests sutProvider.GetDependency().GetByIdAsync(providerOrganization.OrganizationId) .Returns(organization); + sutProvider.GetDependency().ProviderProviderAdmin(provider.Id).Returns(false); + + sutProvider.GetDependency().SeatAdjustmentResultsInPurchase( + provider, + PlanType.TeamsMonthly, + 10).Returns(true); + + var result = await sutProvider.Sut.UpdateAsync(provider.Id, providerOrganizationId, requestBody); + + AssertUnauthorized(result, message: "Service users cannot purchase additional seats."); + } + + [Theory, BitAutoData] + public async Task UpdateAsync_Ok( + Provider provider, + Guid providerOrganizationId, + UpdateClientOrganizationRequestBody requestBody, + ProviderOrganization providerOrganization, + Organization organization, + SutProvider sutProvider) + { + organization.PlanType = PlanType.TeamsMonthly; + organization.Seats = 10; + organization.Status = OrganizationStatusType.Managed; + requestBody.AssignedSeats = 20; + + ConfigureStableProviderServiceUserInputs(provider, sutProvider); + + sutProvider.GetDependency().GetByIdAsync(providerOrganizationId) + .Returns(providerOrganization); + + sutProvider.GetDependency().GetByIdAsync(providerOrganization.OrganizationId) + .Returns(organization); + + sutProvider.GetDependency().ProviderProviderAdmin(provider.Id).Returns(false); + + sutProvider.GetDependency().SeatAdjustmentResultsInPurchase( + provider, + PlanType.TeamsMonthly, + 10).Returns(false); + var result = await sutProvider.Sut.UpdateAsync(provider.Id, providerOrganizationId, requestBody); await sutProvider.GetDependency().Received(1) - .AssignSeatsToClientOrganization( + .ScaleSeats( provider, - organization, - requestBody.AssignedSeats); + PlanType.TeamsMonthly, + 10); await sutProvider.GetDependency().Received(1) - .ReplaceAsync(Arg.Is(org => org.Name == requestBody.Name)); - - Assert.IsType(result); - } - - [Theory, BitAutoData] - public async Task UpdateAsync_Name_Ok( - Provider provider, - Guid providerOrganizationId, - UpdateClientOrganizationRequestBody requestBody, - ProviderOrganization providerOrganization, - Organization organization, - SutProvider sutProvider) - { - ConfigureStableProviderServiceUserInputs(provider, sutProvider); - - sutProvider.GetDependency().GetByIdAsync(providerOrganizationId) - .Returns(providerOrganization); - - sutProvider.GetDependency().GetByIdAsync(providerOrganization.OrganizationId) - .Returns(organization); - - requestBody.AssignedSeats = organization.Seats!.Value; - - var result = await sutProvider.Sut.UpdateAsync(provider.Id, providerOrganizationId, requestBody); - - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() - .AssignSeatsToClientOrganization( - Arg.Any(), - Arg.Any(), - Arg.Any()); - - await sutProvider.GetDependency().Received(1) - .ReplaceAsync(Arg.Is(org => org.Name == requestBody.Name)); + .ReplaceAsync(Arg.Is(org => org.Seats == requestBody.AssignedSeats && org.Name == requestBody.Name)); Assert.IsType(result); } diff --git a/test/Api.Test/Billing/Utilities.cs b/test/Api.Test/Billing/Utilities.cs index 36291ec71..3f5eee72c 100644 --- a/test/Api.Test/Billing/Utilities.cs +++ b/test/Api.Test/Billing/Utilities.cs @@ -25,14 +25,14 @@ public static class Utilities Assert.Equal("Resource not found.", response.Message); } - public static void AssertUnauthorized(IResult result) + public static void AssertUnauthorized(IResult result, string message = "Unauthorized.") { Assert.IsType>(result); var response = (JsonHttpResult)result; Assert.Equal(StatusCodes.Status401Unauthorized, response.StatusCode); - Assert.Equal("Unauthorized.", response.Value.Message); + Assert.Equal(message, response.Value.Message); } public static void ConfigureStableProviderAdminInputs( From dfbc400520f4c474b28e1279a86b2fae1da052a9 Mon Sep 17 00:00:00 2001 From: Matt Bishop Date: Tue, 12 Nov 2024 15:16:31 -0500 Subject: [PATCH 27/29] Remove FCMv1 feature flag (#5030) * Remove FCMv1 feature flag * Killed a using --- src/Core/Constants.cs | 1 - .../NotificationHubPushRegistrationService.cs | 20 ++++--------------- 2 files changed, 4 insertions(+), 17 deletions(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 5830b480e..6978a2c2f 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -111,7 +111,6 @@ public static class FeatureFlagKeys public const string AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section"; public const string EmailVerification = "email-verification"; public const string EmailVerificationDisableTimingDelays = "email-verification-disable-timing-delays"; - public const string AnhFcmv1Migration = "anh-fcmv1-migration"; public const string ExtensionRefresh = "extension-refresh"; public const string RestrictProviderAccess = "restrict-provider-access"; public const string PM4154BulkEncryptionService = "PM-4154-bulk-encryption-service"; diff --git a/src/Core/NotificationHub/NotificationHubPushRegistrationService.cs b/src/Core/NotificationHub/NotificationHubPushRegistrationService.cs index ae32babf4..123152c01 100644 --- a/src/Core/NotificationHub/NotificationHubPushRegistrationService.cs +++ b/src/Core/NotificationHub/NotificationHubPushRegistrationService.cs @@ -4,7 +4,6 @@ using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; using Microsoft.Azure.NotificationHubs; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace Bit.Core.NotificationHub; @@ -60,21 +59,10 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService switch (type) { case DeviceType.Android: - var featureService = _serviceProvider.GetRequiredService(); - if (featureService.IsEnabled(FeatureFlagKeys.AnhFcmv1Migration)) - { - payloadTemplate = "{\"message\":{\"data\":{\"type\":\"$(type)\",\"payload\":\"$(payload)\"}}}"; - messageTemplate = "{\"message\":{\"data\":{\"type\":\"$(type)\"}," + - "\"notification\":{\"title\":\"$(title)\",\"body\":\"$(message)\"}}}"; - installation.Platform = NotificationPlatform.FcmV1; - } - else - { - payloadTemplate = "{\"data\":{\"data\":{\"type\":\"#(type)\",\"payload\":\"$(payload)\"}}}"; - messageTemplate = "{\"data\":{\"data\":{\"type\":\"#(type)\"}," + - "\"notification\":{\"title\":\"$(title)\",\"body\":\"$(message)\"}}}"; - installation.Platform = NotificationPlatform.Fcm; - } + payloadTemplate = "{\"message\":{\"data\":{\"type\":\"$(type)\",\"payload\":\"$(payload)\"}}}"; + messageTemplate = "{\"message\":{\"data\":{\"type\":\"$(type)\"}," + + "\"notification\":{\"title\":\"$(title)\",\"body\":\"$(message)\"}}}"; + installation.Platform = NotificationPlatform.FcmV1; break; case DeviceType.iOS: payloadTemplate = "{\"data\":{\"type\":\"#(type)\",\"payload\":\"$(payload)\"}," + From b1776240ea226039b517f8684cbc2cbbf61ea8b3 Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Wed, 13 Nov 2024 13:19:16 +0100 Subject: [PATCH 28/29] Pm 14861 vault items fail to load (#5031) * Resolve vault items fail to load * Add hasSubscription to metadata * Remove unused property * Fix the failing unit test --- .../Responses/OrganizationMetadataResponse.cs | 6 ++++-- src/Core/Billing/Models/OrganizationMetadata.cs | 3 ++- .../Implementations/OrganizationBillingService.cs | 15 +++++++++++---- .../OrganizationBillingControllerTests.cs | 3 ++- 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/Api/Billing/Models/Responses/OrganizationMetadataResponse.cs b/src/Api/Billing/Models/Responses/OrganizationMetadataResponse.cs index 960cd53ea..86cbdb92c 100644 --- a/src/Api/Billing/Models/Responses/OrganizationMetadataResponse.cs +++ b/src/Api/Billing/Models/Responses/OrganizationMetadataResponse.cs @@ -6,12 +6,14 @@ public record OrganizationMetadataResponse( bool IsEligibleForSelfHost, bool IsManaged, bool IsOnSecretsManagerStandalone, - bool IsSubscriptionUnpaid) + bool IsSubscriptionUnpaid, + bool HasSubscription) { public static OrganizationMetadataResponse From(OrganizationMetadata metadata) => new( metadata.IsEligibleForSelfHost, metadata.IsManaged, metadata.IsOnSecretsManagerStandalone, - metadata.IsSubscriptionUnpaid); + metadata.IsSubscriptionUnpaid, + metadata.HasSubscription); } diff --git a/src/Core/Billing/Models/OrganizationMetadata.cs b/src/Core/Billing/Models/OrganizationMetadata.cs index 138fb6aef..5bdb450dc 100644 --- a/src/Core/Billing/Models/OrganizationMetadata.cs +++ b/src/Core/Billing/Models/OrganizationMetadata.cs @@ -4,4 +4,5 @@ public record OrganizationMetadata( bool IsEligibleForSelfHost, bool IsManaged, bool IsOnSecretsManagerStandalone, - bool IsSubscriptionUnpaid); + bool IsSubscriptionUnpaid, + bool HasSubscription); diff --git a/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs b/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs index bdcf0fbf6..eadc58962 100644 --- a/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs +++ b/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs @@ -62,18 +62,25 @@ public class OrganizationBillingService( return null; } + var isEligibleForSelfHost = IsEligibleForSelfHost(organization); + var isManaged = organization.Status == OrganizationStatusType.Managed; + + if (string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId)) + { + return new OrganizationMetadata(isEligibleForSelfHost, isManaged, false, + false, false); + } + var customer = await subscriberService.GetCustomer(organization, new CustomerGetOptions { Expand = ["discount.coupon.applies_to"] }); var subscription = await subscriberService.GetSubscription(organization); - - var isEligibleForSelfHost = IsEligibleForSelfHost(organization); - var isManaged = organization.Status == OrganizationStatusType.Managed; var isOnSecretsManagerStandalone = IsOnSecretsManagerStandalone(organization, customer, subscription); var isSubscriptionUnpaid = IsSubscriptionUnpaid(subscription); + var hasSubscription = true; return new OrganizationMetadata(isEligibleForSelfHost, isManaged, isOnSecretsManagerStandalone, - isSubscriptionUnpaid); + isSubscriptionUnpaid, hasSubscription); } public async Task UpdatePaymentMethod( diff --git a/test/Api.Test/Billing/Controllers/OrganizationBillingControllerTests.cs b/test/Api.Test/Billing/Controllers/OrganizationBillingControllerTests.cs index 51e374fd5..703475fc5 100644 --- a/test/Api.Test/Billing/Controllers/OrganizationBillingControllerTests.cs +++ b/test/Api.Test/Billing/Controllers/OrganizationBillingControllerTests.cs @@ -52,7 +52,7 @@ public class OrganizationBillingControllerTests { sutProvider.GetDependency().OrganizationUser(organizationId).Returns(true); sutProvider.GetDependency().GetMetadata(organizationId) - .Returns(new OrganizationMetadata(true, true, true, true)); + .Returns(new OrganizationMetadata(true, true, true, true, true)); var result = await sutProvider.Sut.GetMetadataAsync(organizationId); @@ -64,6 +64,7 @@ public class OrganizationBillingControllerTests Assert.True(response.IsManaged); Assert.True(response.IsOnSecretsManagerStandalone); Assert.True(response.IsSubscriptionUnpaid); + Assert.True(response.HasSubscription); } [Theory, BitAutoData] From 6f7cdcfcea6ac69e8566699e0f8b9d11b77826b4 Mon Sep 17 00:00:00 2001 From: Jonas Hendrickx Date: Wed, 13 Nov 2024 15:01:26 +0100 Subject: [PATCH 29/29] [PM-13783] Battle harden ProviderType enum expansion (#5004) Co-authored-by: Matt Bishop --- src/Core/Billing/Extensions/BillingExtensions.cs | 5 ++++- src/Core/Context/CurrentContext.cs | 5 +++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Core/Billing/Extensions/BillingExtensions.cs b/src/Core/Billing/Extensions/BillingExtensions.cs index 02e8de924..93afbb971 100644 --- a/src/Core/Billing/Extensions/BillingExtensions.cs +++ b/src/Core/Billing/Extensions/BillingExtensions.cs @@ -14,7 +14,10 @@ public static class BillingExtensions provider.SupportsConsolidatedBilling() && provider.Status == ProviderStatusType.Billable; public static bool SupportsConsolidatedBilling(this Provider provider) - => provider.Type is ProviderType.Msp or ProviderType.MultiOrganizationEnterprise; + => provider.Type.SupportsConsolidatedBilling(); + + public static bool SupportsConsolidatedBilling(this ProviderType providerType) + => providerType is ProviderType.Msp or ProviderType.MultiOrganizationEnterprise; public static bool IsValidClient(this Organization organization) => organization is diff --git a/src/Core/Context/CurrentContext.cs b/src/Core/Context/CurrentContext.cs index e64615753..2767b5925 100644 --- a/src/Core/Context/CurrentContext.cs +++ b/src/Core/Context/CurrentContext.cs @@ -3,6 +3,7 @@ using Bit.Core.AdminConsole.Context; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Models.Data.Provider; using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Billing.Extensions; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Identity; @@ -363,9 +364,9 @@ public class CurrentContext : ICurrentContext public async Task ViewSubscription(Guid orgId) { - var orgManagedByMspProvider = (await GetOrganizationProviderDetails()).Any(po => po.OrganizationId == orgId && po.ProviderType == ProviderType.Msp); + var isManagedByBillableProvider = (await GetOrganizationProviderDetails()).Any(po => po.OrganizationId == orgId && po.ProviderType.SupportsConsolidatedBilling()); - return orgManagedByMspProvider + return isManagedByBillableProvider ? await ProviderUserForOrgAsync(orgId) : await OrganizationOwner(orgId); }