diff --git a/.github/workflows/_move_finalization_db_scripts.yml b/.github/workflows/_move_finalization_db_scripts.yml index c54e3abb2..3eb3777ce 100644 --- a/.github/workflows/_move_finalization_db_scripts.yml +++ b/.github/workflows/_move_finalization_db_scripts.yml @@ -30,7 +30,7 @@ jobs: secrets: "github-pat-bitwarden-devops-bot-repo-scope" - name: Check out branch - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: token: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }} @@ -54,7 +54,7 @@ jobs: if: ${{ needs.setup.outputs.copy_finalization_scripts == 'true' }} steps: - name: Checkout - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: fetch-depth: 0 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4ba3ec22b..6df666417 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Check out repo - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - name: Set up .NET uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1 @@ -68,7 +68,7 @@ jobs: node: true steps: - name: Check out repo - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - name: Set up .NET uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1 @@ -110,7 +110,7 @@ jobs: ls -atlh ../../../ - name: Upload project artifact - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: ${{ matrix.project_name }}.zip path: ${{ matrix.base_path }}/${{ matrix.project_name }}/${{ matrix.project_name }}.zip @@ -173,7 +173,7 @@ jobs: dotnet: true steps: - name: Check out repo - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - name: Check branch to publish env: @@ -263,7 +263,7 @@ jobs: -d ${{ matrix.base_path }}/${{ matrix.project_name }}/obj/build-output/publish - name: Build Docker image - uses: docker/build-push-action@32945a339266b759abcbdc89316275140b0fc960 # v6.8.0 + uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6.9.0 with: context: ${{ matrix.base_path }}/${{ matrix.project_name }} file: ${{ matrix.base_path }}/${{ matrix.project_name }}/Dockerfile @@ -275,14 +275,14 @@ jobs: - name: Scan Docker image id: container-scan - uses: anchore/scan-action@64a33b277ea7a1215a3c142735a1091341939ff5 # v4.1.2 + uses: anchore/scan-action@49e50b215b647c5ec97abb66f69af73c46a4ca08 # v5.0.1 with: image: ${{ steps.image-tags.outputs.primary_tag }} fail-build: false output-format: sarif - name: Upload Grype results to GitHub - uses: github/codeql-action/upload-sarif@461ef6c76dfe95d5c364de2f431ddbd31a417628 # v3.26.9 + uses: github/codeql-action/upload-sarif@f779452ac5af1c261dce0346a8f964149f49322b # v3.26.13 with: sarif_file: ${{ steps.container-scan.outputs.sarif }} @@ -292,7 +292,7 @@ jobs: needs: build-docker steps: - name: Check out repo - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - name: Set up .NET uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1 @@ -311,7 +311,7 @@ jobs: github.ref == 'refs/heads/hotfix-rc' run: | # Set proper setup image based on branch - case "${{ github.ref }}" in + case "$GITHUB_REF" in "refs/heads/main") SETUP_IMAGE="$_AZ_REGISTRY/setup:dev" ;; @@ -355,7 +355,7 @@ jobs: - name: Upload Docker stub US artifact if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc' - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: docker-stub-US.zip path: docker-stub-US.zip @@ -363,7 +363,7 @@ jobs: - name: Upload Docker stub EU artifact if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc' - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: docker-stub-EU.zip path: docker-stub-EU.zip @@ -371,7 +371,7 @@ jobs: - name: Upload Docker stub US checksum artifact if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc' - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: docker-stub-US-sha256.txt path: docker-stub-US-sha256.txt @@ -379,7 +379,7 @@ jobs: - name: Upload Docker stub EU checksum artifact if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc' - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: docker-stub-EU-sha256.txt path: docker-stub-EU-sha256.txt @@ -403,7 +403,7 @@ jobs: GLOBALSETTINGS__SQLSERVER__CONNECTIONSTRING: "placeholder" - name: Upload Public API Swagger artifact - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: swagger.json path: swagger.json @@ -437,14 +437,14 @@ jobs: GLOBALSETTINGS__SQLSERVER__CONNECTIONSTRING: "placeholder" - name: Upload Internal API Swagger artifact - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: internal.json path: internal.json if-no-files-found: error - name: Upload Identity Swagger artifact - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: identity.json path: identity.json @@ -467,7 +467,7 @@ jobs: - win-x64 steps: - name: Check out repo - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - name: Set up .NET uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1 @@ -486,7 +486,7 @@ jobs: - name: Upload project artifact for Windows if: ${{ contains(matrix.target, 'win') == true }} - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: MsSqlMigratorUtility-${{ matrix.target }} path: util/MsSqlMigratorUtility/obj/build-output/publish/MsSqlMigratorUtility.exe @@ -494,7 +494,7 @@ jobs: - name: Upload project artifact if: ${{ contains(matrix.target, 'win') == false }} - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: MsSqlMigratorUtility-${{ matrix.target }} path: util/MsSqlMigratorUtility/obj/build-output/publish/MsSqlMigratorUtility @@ -528,9 +528,9 @@ jobs: workflow_id: 'build-unified.yml', ref: 'main', inputs: { - server_branch: '${{ github.ref }}' + server_branch: process.env.GITHUB_REF } - }) + }); trigger-k8s-deploy: name: Trigger k8s deploy diff --git a/.github/workflows/cleanup-rc-branch.yml b/.github/workflows/cleanup-rc-branch.yml index 3b3c2d55d..1eba867a9 100644 --- a/.github/workflows/cleanup-rc-branch.yml +++ b/.github/workflows/cleanup-rc-branch.yml @@ -24,7 +24,7 @@ jobs: secrets: "github-pat-bitwarden-devops-bot-repo-scope" - name: Checkout main - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: ref: main token: ${{ steps.retrieve-bot-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }} diff --git a/.github/workflows/code-references.yml b/.github/workflows/code-references.yml index 101e5730d..855241fdb 100644 --- a/.github/workflows/code-references.yml +++ b/.github/workflows/code-references.yml @@ -33,7 +33,7 @@ jobs: steps: - name: Check out repository - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - name: Collect id: collect diff --git a/.github/workflows/protect-files.yml b/.github/workflows/protect-files.yml index 3bbc7e74f..10924f656 100644 --- a/.github/workflows/protect-files.yml +++ b/.github/workflows/protect-files.yml @@ -29,7 +29,7 @@ jobs: label: "DB-migrations-changed" steps: - name: Check out repo - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: fetch-depth: 2 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 3c45f84b7..77ea9dca4 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -99,7 +99,7 @@ jobs: echo "Github Release Option: $RELEASE_OPTION" - name: Check out repo - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - name: Set up project name id: setup diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c63302cbc..0c89a01c2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -37,7 +37,7 @@ jobs: fi - name: Check out repo - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - name: Check release version id: version diff --git a/.github/workflows/repository-management.yml b/.github/workflows/repository-management.yml index 29860b868..eb4187c59 100644 --- a/.github/workflows/repository-management.yml +++ b/.github/workflows/repository-management.yml @@ -27,7 +27,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Check out target ref - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: ref: ${{ inputs.target_ref }} @@ -62,7 +62,7 @@ jobs: version: ${{ inputs.version_number_override }} - name: Check out branch - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: ref: main @@ -150,7 +150,7 @@ jobs: needs: bump_version steps: - name: Check out main branch - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: ref: main diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml index 0f4d060ba..8703bac5e 100644 --- a/.github/workflows/scan.yml +++ b/.github/workflows/scan.yml @@ -26,12 +26,12 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: ref: ${{ github.event.pull_request.head.sha }} - name: Scan with Checkmarx - uses: checkmarx/ast-github-action@9fda5a4a2c297608117a5a56af424502a9192e57 # 2.0.34 + uses: checkmarx/ast-github-action@f0869bd1a37fddc06499a096101e6c900e815d81 # 2.0.36 env: INCREMENTAL: "${{ contains(github.event_name, 'pull_request') && '--sast-incremental' || '' }}" with: @@ -46,7 +46,7 @@ jobs: --output-path . ${{ env.INCREMENTAL }} - name: Upload Checkmarx results to GitHub - uses: github/codeql-action/upload-sarif@461ef6c76dfe95d5c364de2f431ddbd31a417628 # v3.26.9 + uses: github/codeql-action/upload-sarif@f779452ac5af1c261dce0346a8f964149f49322b # v3.26.13 with: sarif_file: cx_result.sarif @@ -66,7 +66,7 @@ jobs: distribution: "zulu" - name: Check out repo - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: fetch-depth: 0 ref: ${{ github.event.pull_request.head.sha }} diff --git a/.github/workflows/test-database.yml b/.github/workflows/test-database.yml index 325f10b94..09a4b7a18 100644 --- a/.github/workflows/test-database.yml +++ b/.github/workflows/test-database.yml @@ -36,7 +36,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Check out repo - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - name: Set up .NET uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1 @@ -147,7 +147,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Check out repo - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - name: Set up .NET uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1 @@ -164,7 +164,7 @@ jobs: shell: pwsh - name: Upload DACPAC - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: sql.dacpac path: Sql.dacpac @@ -190,7 +190,7 @@ jobs: shell: pwsh - name: Report validation results - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: report.xml path: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 216130a21..bd9e358df 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -46,7 +46,7 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - name: Set up .NET uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1 @@ -77,7 +77,7 @@ jobs: fail-on-error: true - name: Upload to codecov.io - uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0 + uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4.6.0 if: ${{ needs.check-test-secrets.outputs.available == 'true' }} env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/src/Api/AdminConsole/Controllers/OrganizationsController.cs b/src/Api/AdminConsole/Controllers/OrganizationsController.cs index b7009022f..3d206da44 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationsController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationsController.cs @@ -124,7 +124,11 @@ public class OrganizationsController : Controller var userId = _userService.GetProperUserId(User).Value; var organizations = await _organizationUserRepository.GetManyDetailsByUserAsync(userId, OrganizationUserStatusType.Confirmed); - var responses = organizations.Select(o => new ProfileOrganizationResponseModel(o)); + + var organizationManagingActiveUser = await _userService.GetOrganizationsManagingUserAsync(userId); + var organizationIdsManagingActiveUser = organizationManagingActiveUser.Select(o => o.Id); + + var responses = organizations.Select(o => new ProfileOrganizationResponseModel(o, organizationIdsManagingActiveUser)); return new ListResponseModel(responses); } diff --git a/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs b/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs index 17ebfc095..a573bfb8d 100644 --- a/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs @@ -15,7 +15,10 @@ public class ProfileOrganizationResponseModel : ResponseModel { public ProfileOrganizationResponseModel(string str) : base(str) { } - public ProfileOrganizationResponseModel(OrganizationUserOrganizationDetails organization) : this("profileOrganization") + public ProfileOrganizationResponseModel( + OrganizationUserOrganizationDetails organization, + IEnumerable organizationIdsManagingUser) + : this("profileOrganization") { Id = organization.OrganizationId; Name = organization.Name; @@ -64,6 +67,7 @@ public class ProfileOrganizationResponseModel : ResponseModel AccessSecretsManager = organization.AccessSecretsManager; LimitCollectionCreationDeletion = organization.LimitCollectionCreationDeletion; AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems; + UserIsManagedByOrganization = organizationIdsManagingUser.Contains(organization.OrganizationId); if (organization.SsoConfig != null) { @@ -122,4 +126,15 @@ public class ProfileOrganizationResponseModel : ResponseModel public bool AccessSecretsManager { get; set; } public bool LimitCollectionCreationDeletion { get; set; } public bool AllowAdminAccessToAllCollectionItems { get; set; } + /// + /// Indicates if the organization manages the user. + /// + /// + /// An organization manages a user if the user's email domain is verified by the organization and the user is a member of it. + /// The organization must be enabled and able to have verified domains. + /// + /// + /// False if the Account Deprovisioning feature flag is disabled. + /// + public bool UserIsManagedByOrganization { get; set; } } diff --git a/src/Api/Auth/Controllers/AccountsController.cs b/src/Api/Auth/Controllers/AccountsController.cs index cf74460fc..a0c01752a 100644 --- a/src/Api/Auth/Controllers/AccountsController.cs +++ b/src/Api/Auth/Controllers/AccountsController.cs @@ -443,11 +443,11 @@ public class AccountsController : Controller var twoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user); var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user); - var managedByOrganizationId = await GetManagedByOrganizationIdAsync(user); + var organizationIdsManagingActiveUser = await GetOrganizationIdsManagingUserAsync(user.Id); var response = new ProfileResponseModel(user, organizationUserDetails, providerUserDetails, providerUserOrganizationDetails, twoFactorEnabled, - hasPremiumFromOrg, managedByOrganizationId); + hasPremiumFromOrg, organizationIdsManagingActiveUser); return response; } @@ -457,7 +457,9 @@ public class AccountsController : Controller var userId = _userService.GetProperUserId(User); var organizationUserDetails = await _organizationUserRepository.GetManyDetailsByUserAsync(userId.Value, OrganizationUserStatusType.Confirmed); - var responseData = organizationUserDetails.Select(o => new ProfileOrganizationResponseModel(o)); + var organizationIdsManagingActiveUser = await GetOrganizationIdsManagingUserAsync(userId.Value); + + var responseData = organizationUserDetails.Select(o => new ProfileOrganizationResponseModel(o, organizationIdsManagingActiveUser)); return new ListResponseModel(responseData); } @@ -475,9 +477,9 @@ public class AccountsController : Controller var twoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user); var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user); - var managedByOrganizationId = await GetManagedByOrganizationIdAsync(user); + var organizationIdsManagingActiveUser = await GetOrganizationIdsManagingUserAsync(user.Id); - var response = new ProfileResponseModel(user, null, null, null, twoFactorEnabled, hasPremiumFromOrg, managedByOrganizationId); + var response = new ProfileResponseModel(user, null, null, null, twoFactorEnabled, hasPremiumFromOrg, organizationIdsManagingActiveUser); return response; } @@ -494,9 +496,9 @@ public class AccountsController : Controller var userTwoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user); var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user); - var managedByOrganizationId = await GetManagedByOrganizationIdAsync(user); + var organizationIdsManagingActiveUser = await GetOrganizationIdsManagingUserAsync(user.Id); - var response = new ProfileResponseModel(user, null, null, null, userTwoFactorEnabled, userHasPremiumFromOrganization, managedByOrganizationId); + var response = new ProfileResponseModel(user, null, null, null, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsManagingActiveUser); return response; } @@ -647,9 +649,9 @@ public class AccountsController : Controller var userTwoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user); var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user); - var managedByOrganizationId = await GetManagedByOrganizationIdAsync(user); + var organizationIdsManagingActiveUser = await GetOrganizationIdsManagingUserAsync(user.Id); - var profile = new ProfileResponseModel(user, null, null, null, userTwoFactorEnabled, userHasPremiumFromOrganization, managedByOrganizationId); + var profile = new ProfileResponseModel(user, null, null, null, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsManagingActiveUser); return new PaymentResponseModel { UserProfile = profile, @@ -937,14 +939,9 @@ public class AccountsController : Controller } } - private async Task GetManagedByOrganizationIdAsync(User user) + private async Task> GetOrganizationIdsManagingUserAsync(Guid userId) { - if (!_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)) - { - return null; - } - - var organizationManagingUser = await _userService.GetOrganizationManagingUserAsync(user.Id); - return organizationManagingUser?.Id; + var organizationManagingUser = await _userService.GetOrganizationsManagingUserAsync(userId); + return organizationManagingUser.Select(o => o.Id); } } diff --git a/src/Api/Billing/Controllers/OrganizationsController.cs b/src/Api/Billing/Controllers/OrganizationsController.cs index 5371186b1..75ae2fb89 100644 --- a/src/Api/Billing/Controllers/OrganizationsController.cs +++ b/src/Api/Billing/Controllers/OrganizationsController.cs @@ -201,7 +201,10 @@ public class OrganizationsController( var organizationDetails = await organizationUserRepository.GetDetailsByUserAsync(userId, organization.Id, OrganizationUserStatusType.Confirmed); - return new ProfileOrganizationResponseModel(organizationDetails); + var organizationManagingActiveUser = await userService.GetOrganizationsManagingUserAsync(userId); + var organizationIdsManagingActiveUser = organizationManagingActiveUser.Select(o => o.Id); + + return new ProfileOrganizationResponseModel(organizationDetails, organizationIdsManagingActiveUser); } [HttpPost("{id:guid}/seat")] diff --git a/src/Api/Models/Response/ProfileResponseModel.cs b/src/Api/Models/Response/ProfileResponseModel.cs index f5d0382e5..a6ed4ebfa 100644 --- a/src/Api/Models/Response/ProfileResponseModel.cs +++ b/src/Api/Models/Response/ProfileResponseModel.cs @@ -15,7 +15,7 @@ public class ProfileResponseModel : ResponseModel IEnumerable providerUserOrganizationDetails, bool twoFactorEnabled, bool premiumFromOrganization, - Guid? managedByOrganizationId) : base("profile") + IEnumerable organizationIdsManagingUser) : base("profile") { if (user == null) { @@ -37,11 +37,10 @@ public class ProfileResponseModel : ResponseModel UsesKeyConnector = user.UsesKeyConnector; AvatarColor = user.AvatarColor; CreationDate = user.CreationDate; - Organizations = organizationsUserDetails?.Select(o => new ProfileOrganizationResponseModel(o)); + Organizations = organizationsUserDetails?.Select(o => new ProfileOrganizationResponseModel(o, organizationIdsManagingUser)); Providers = providerUserDetails?.Select(p => new ProfileProviderResponseModel(p)); ProviderOrganizations = providerUserOrganizationDetails?.Select(po => new ProfileProviderOrganizationResponseModel(po)); - ManagedByOrganizationId = managedByOrganizationId; } public ProfileResponseModel() : base("profile") @@ -63,7 +62,6 @@ public class ProfileResponseModel : ResponseModel public bool UsesKeyConnector { get; set; } public string AvatarColor { get; set; } public DateTime CreationDate { get; set; } - public Guid? ManagedByOrganizationId { get; set; } public IEnumerable Organizations { get; set; } public IEnumerable Providers { get; set; } public IEnumerable ProviderOrganizations { get; set; } diff --git a/src/Api/Vault/Controllers/CiphersController.cs b/src/Api/Vault/Controllers/CiphersController.cs index 769ba34a1..09ade4d0d 100644 --- a/src/Api/Vault/Controllers/CiphersController.cs +++ b/src/Api/Vault/Controllers/CiphersController.cs @@ -910,6 +910,13 @@ public class CiphersController : Controller throw new BadRequestException(ModelState); } + // If Account Deprovisioning is enabled, we need to check if the user is managed by any organization. + if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) + && await _userService.IsManagedByAnyOrganizationAsync(user.Id)) + { + throw new BadRequestException("Cannot purge accounts owned by an organization. Contact your organization administrator for additional details."); + } + if (string.IsNullOrWhiteSpace(organizationId)) { await _cipherRepository.DeleteByUserIdAsync(user.Id); diff --git a/src/Api/Vault/Controllers/SyncController.cs b/src/Api/Vault/Controllers/SyncController.cs index 79c71bb87..853320ec6 100644 --- a/src/Api/Vault/Controllers/SyncController.cs +++ b/src/Api/Vault/Controllers/SyncController.cs @@ -1,5 +1,4 @@ using Bit.Api.Vault.Models.Response; -using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Repositories; @@ -7,7 +6,6 @@ using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Data; -using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; @@ -95,23 +93,12 @@ public class SyncController : Controller var userTwoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user); var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user); - var managedByOrganizationId = await GetManagedByOrganizationIdAsync(user, organizationUserDetails); + var organizationManagingActiveUser = await _userService.GetOrganizationsManagingUserAsync(user.Id); + var organizationIdsManagingActiveUser = organizationManagingActiveUser.Select(o => o.Id); var response = new SyncResponseModel(_globalSettings, user, userTwoFactorEnabled, userHasPremiumFromOrganization, - managedByOrganizationId, organizationUserDetails, providerUserDetails, providerUserOrganizationDetails, + organizationIdsManagingActiveUser, organizationUserDetails, providerUserDetails, providerUserOrganizationDetails, folders, collections, ciphers, collectionCiphersGroupDict, excludeDomains, policies, sends); return response; } - - private async Task GetManagedByOrganizationIdAsync(User user, IEnumerable organizationUserDetails) - { - if (!_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) || - !organizationUserDetails.Any(o => o.Enabled && o.UseSso)) - { - return null; - } - - var organizationManagingUser = await _userService.GetOrganizationManagingUserAsync(user.Id); - return organizationManagingUser?.Id; - } } diff --git a/src/Api/Vault/Models/Response/SyncResponseModel.cs b/src/Api/Vault/Models/Response/SyncResponseModel.cs index 2170a5232..ce5f4562d 100644 --- a/src/Api/Vault/Models/Response/SyncResponseModel.cs +++ b/src/Api/Vault/Models/Response/SyncResponseModel.cs @@ -21,7 +21,7 @@ public class SyncResponseModel : ResponseModel User user, bool userTwoFactorEnabled, bool userHasPremiumFromOrganization, - Guid? managedByOrganizationId, + IEnumerable organizationIdsManagingUser, IEnumerable organizationUserDetails, IEnumerable providerUserDetails, IEnumerable providerUserOrganizationDetails, @@ -35,7 +35,7 @@ public class SyncResponseModel : ResponseModel : base("sync") { Profile = new ProfileResponseModel(user, organizationUserDetails, providerUserDetails, - providerUserOrganizationDetails, userTwoFactorEnabled, userHasPremiumFromOrganization, managedByOrganizationId); + providerUserOrganizationDetails, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsManagingUser); Folders = folders.Select(f => new FolderResponseModel(f)); Ciphers = ciphers.Select(c => new CipherDetailsResponseModel(c, globalSettings, collectionCiphersDict)); Collections = collections?.Select( diff --git a/src/Core/AdminConsole/Repositories/IOrganizationRepository.cs b/src/Core/AdminConsole/Repositories/IOrganizationRepository.cs index 9c14c4fbd..5b274d3f8 100644 --- a/src/Core/AdminConsole/Repositories/IOrganizationRepository.cs +++ b/src/Core/AdminConsole/Repositories/IOrganizationRepository.cs @@ -19,7 +19,7 @@ public interface IOrganizationRepository : IRepository Task> GetOwnerEmailAddressesById(Guid organizationId); /// - /// Gets the organization that has a claimed domain matching the user's email domain. + /// Gets the organizations that have a verified domain matching the user's email domain. /// - Task GetByClaimedUserDomainAsync(Guid userId); + Task> GetByVerifiedUserEmailDomainAsync(Guid userId); } diff --git a/src/Core/Services/IUserService.cs b/src/Core/Services/IUserService.cs index b15f5153e..65bec5ea9 100644 --- a/src/Core/Services/IUserService.cs +++ b/src/Core/Services/IUserService.cs @@ -90,14 +90,20 @@ public interface IUserService /// Indicates if the user is managed by any organization. /// /// - /// A managed user is a user whose email domain matches one of the Organization's verified domains. - /// The organization must be enabled and be on an Enterprise plan. + /// A user is considered managed by an organization if their email domain matches one of the verified domains of that organization, and the user is a member of it. + /// The organization must be enabled and able to have verified domains. /// + /// + /// False if the Account Deprovisioning feature flag is disabled. + /// Task IsManagedByAnyOrganizationAsync(Guid userId); /// - /// Gets the organization that manages the user. + /// Gets the organizations that manage the user. /// + /// + /// An empty collection if the Account Deprovisioning feature flag is disabled. + /// /// - Task GetOrganizationManagingUserAsync(Guid userId); + Task> GetOrganizationsManagingUserAsync(Guid userId); } diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 413437a59..f2e1d183d 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -1267,18 +1267,24 @@ public class UserService : UserManager, IUserService, IDisposable public async Task IsManagedByAnyOrganizationAsync(Guid userId) { - var managingOrganization = await GetOrganizationManagingUserAsync(userId); - return managingOrganization != null; + var managingOrganizations = await GetOrganizationsManagingUserAsync(userId); + return managingOrganizations.Any(); } - public async Task GetOrganizationManagingUserAsync(Guid userId) + public async Task> GetOrganizationsManagingUserAsync(Guid userId) { - // Users can only be managed by an Organization that is enabled and can have organization domains - var organization = await _organizationRepository.GetByClaimedUserDomainAsync(userId); + if (!_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)) + { + return Enumerable.Empty(); + } + // Get all organizations that have verified the user's email domain. + var organizationsWithVerifiedUserEmailDomain = await _organizationRepository.GetByVerifiedUserEmailDomainAsync(userId); + + // Organizations must be enabled and able to have verified domains. // TODO: Replace "UseSso" with a new organization ability like "UseOrganizationDomains" (PM-11622). // Verified domains were tied to SSO, so we currently check the "UseSso" organization ability. - return (organization is { Enabled: true, UseSso: true }) ? organization : null; + return organizationsWithVerifiedUserEmailDomain.Where(organization => organization is { Enabled: true, UseSso: true }); } /// diff --git a/src/Identity/Identity.csproj b/src/Identity/Identity.csproj index 584dd78d6..cb506d86e 100644 --- a/src/Identity/Identity.csproj +++ b/src/Identity/Identity.csproj @@ -12,8 +12,4 @@ - - - - diff --git a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationRepository.cs b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationRepository.cs index bdc2fb4ca..20fdf8315 100644 --- a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationRepository.cs +++ b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationRepository.cs @@ -168,7 +168,7 @@ public class OrganizationRepository : Repository, IOrganizat commandType: CommandType.StoredProcedure); } - public async Task GetByClaimedUserDomainAsync(Guid userId) + public async Task> GetByVerifiedUserEmailDomainAsync(Guid userId) { using (var connection = new SqlConnection(ConnectionString)) { @@ -177,7 +177,7 @@ public class OrganizationRepository : Repository, IOrganizat new { UserId = userId }, commandType: CommandType.StoredProcedure); - return result.SingleOrDefault(); + return result.ToList(); } } } diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs index 96c9a912e..bb9090e0a 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs @@ -276,7 +276,7 @@ public class OrganizationRepository : Repository GetByClaimedUserDomainAsync(Guid userId) + public async Task> GetByVerifiedUserEmailDomainAsync(Guid userId) { using (var scope = ServiceScopeFactory.CreateScope()) { @@ -291,7 +291,7 @@ public class OrganizationRepository : Repository sutProvider, Guid userId, Organization organization) + public async Task IsManagedByAnyOrganizationAsync_WithAccountDeprovisioningDisabled_ReturnsFalse( + SutProvider sutProvider, Guid userId) { - organization.Enabled = true; - organization.UseSso = true; - - sutProvider.GetDependency() - .GetByClaimedUserDomainAsync(userId) - .Returns(organization); - - var result = await sutProvider.Sut.IsManagedByAnyOrganizationAsync(userId); - Assert.True(result); - } - - [Theory, BitAutoData] - public async Task IsManagedByAnyOrganizationAsync_WithManagingDisabledOrganization_ReturnsFalse( - SutProvider sutProvider, Guid userId, Organization organization) - { - organization.Enabled = false; - organization.UseSso = true; - - sutProvider.GetDependency() - .GetByClaimedUserDomainAsync(userId) - .Returns(organization); + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) + .Returns(false); var result = await sutProvider.Sut.IsManagedByAnyOrganizationAsync(userId); Assert.False(result); } [Theory, BitAutoData] - public async Task IsManagedByAnyOrganizationAsync_WithOrganizationUseSsoFalse_ReturnsFalse( + public async Task IsManagedByAnyOrganizationAsync_WithAccountDeprovisioningEnabled_WithManagingEnabledOrganization_ReturnsTrue( + SutProvider sutProvider, Guid userId, Organization organization) + { + organization.Enabled = true; + organization.UseSso = true; + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) + .Returns(true); + + sutProvider.GetDependency() + .GetByVerifiedUserEmailDomainAsync(userId) + .Returns(new[] { organization }); + + var result = await sutProvider.Sut.IsManagedByAnyOrganizationAsync(userId); + Assert.True(result); + } + + [Theory, BitAutoData] + public async Task IsManagedByAnyOrganizationAsync_WithAccountDeprovisioningEnabled_WithManagingDisabledOrganization_ReturnsFalse( + SutProvider sutProvider, Guid userId, Organization organization) + { + organization.Enabled = false; + organization.UseSso = true; + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) + .Returns(true); + + sutProvider.GetDependency() + .GetByVerifiedUserEmailDomainAsync(userId) + .Returns(new[] { organization }); + + var result = await sutProvider.Sut.IsManagedByAnyOrganizationAsync(userId); + Assert.False(result); + } + + [Theory, BitAutoData] + public async Task IsManagedByAnyOrganizationAsync_WithAccountDeprovisioningEnabled_WithOrganizationUseSsoFalse_ReturnsFalse( SutProvider sutProvider, Guid userId, Organization organization) { organization.Enabled = true; organization.UseSso = false; + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) + .Returns(true); + sutProvider.GetDependency() - .GetByClaimedUserDomainAsync(userId) - .Returns(organization); + .GetByVerifiedUserEmailDomainAsync(userId) + .Returns(new[] { organization }); var result = await sutProvider.Sut.IsManagedByAnyOrganizationAsync(userId); Assert.False(result); diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationRepositoryTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationRepositoryTests.cs index eac71e9c2..f6dc4a989 100644 --- a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationRepositoryTests.cs @@ -97,13 +97,160 @@ public class OrganizationRepositoryTests ResetPasswordKey = "resetpasswordkey1", }); - var user1Response = await organizationRepository.GetByClaimedUserDomainAsync(user1.Id); - var user2Response = await organizationRepository.GetByClaimedUserDomainAsync(user2.Id); - var user3Response = await organizationRepository.GetByClaimedUserDomainAsync(user3.Id); + var user1Response = await organizationRepository.GetByVerifiedUserEmailDomainAsync(user1.Id); + var user2Response = await organizationRepository.GetByVerifiedUserEmailDomainAsync(user2.Id); + var user3Response = await organizationRepository.GetByVerifiedUserEmailDomainAsync(user3.Id); - Assert.NotNull(user1Response); - Assert.Equal(organization.Id, user1Response.Id); - Assert.Null(user2Response); - Assert.Null(user3Response); + Assert.NotEmpty(user1Response); + Assert.Equal(organization.Id, user1Response.First().Id); + Assert.Empty(user2Response); + Assert.Empty(user3Response); + } + + [DatabaseTheory, DatabaseData] + public async Task GetByVerifiedUserEmailDomainAsync_WithUnverifiedDomains_ReturnsEmpty( + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationDomainRepository organizationDomainRepository) + { + var id = Guid.NewGuid(); + var domainName = $"{id}.example.com"; + + var user = await userRepository.CreateAsync(new User + { + Name = "Test User", + Email = $"test+{id}@{domainName}", + ApiKey = "TEST", + SecurityStamp = "stamp", + Kdf = KdfType.PBKDF2_SHA256, + KdfIterations = 1, + KdfMemory = 2, + KdfParallelism = 3 + }); + + var organization = await organizationRepository.CreateAsync(new Organization + { + Name = $"Test Org {id}", + BillingEmail = user.Email, + Plan = "Test", + PrivateKey = "privatekey", + }); + + var organizationDomain = new OrganizationDomain + { + OrganizationId = organization.Id, + DomainName = domainName, + Txt = "btw+12345", + }; + organizationDomain.SetNextRunDate(12); + organizationDomain.SetJobRunCount(); + await organizationDomainRepository.CreateAsync(organizationDomain); + + await organizationUserRepository.CreateAsync(new OrganizationUser + { + OrganizationId = organization.Id, + UserId = user.Id, + Status = OrganizationUserStatusType.Confirmed, + ResetPasswordKey = "resetpasswordkey", + }); + + var result = await organizationRepository.GetByVerifiedUserEmailDomainAsync(user.Id); + + Assert.Empty(result); + } + + [DatabaseTheory, DatabaseData] + public async Task GetByVerifiedUserEmailDomainAsync_WithMultipleVerifiedDomains_ReturnsAllMatchingOrganizations( + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationDomainRepository organizationDomainRepository) + { + var id = Guid.NewGuid(); + var domainName = $"{id}.example.com"; + + var user = await userRepository.CreateAsync(new User + { + Name = "Test User", + Email = $"test+{id}@{domainName}", + ApiKey = "TEST", + SecurityStamp = "stamp", + Kdf = KdfType.PBKDF2_SHA256, + KdfIterations = 1, + KdfMemory = 2, + KdfParallelism = 3 + }); + + var organization1 = await organizationRepository.CreateAsync(new Organization + { + Name = $"Test Org 1 {id}", + BillingEmail = user.Email, + Plan = "Test", + PrivateKey = "privatekey1", + }); + + var organization2 = await organizationRepository.CreateAsync(new Organization + { + Name = $"Test Org 2 {id}", + BillingEmail = user.Email, + Plan = "Test", + PrivateKey = "privatekey2", + }); + + var organizationDomain1 = new OrganizationDomain + { + OrganizationId = organization1.Id, + DomainName = domainName, + Txt = "btw+12345", + }; + organizationDomain1.SetNextRunDate(12); + organizationDomain1.SetJobRunCount(); + organizationDomain1.SetVerifiedDate(); + await organizationDomainRepository.CreateAsync(organizationDomain1); + + var organizationDomain2 = new OrganizationDomain + { + OrganizationId = organization2.Id, + DomainName = domainName, + Txt = "btw+67890", + }; + organizationDomain2.SetNextRunDate(12); + organizationDomain2.SetJobRunCount(); + organizationDomain2.SetVerifiedDate(); + await organizationDomainRepository.CreateAsync(organizationDomain2); + + await organizationUserRepository.CreateAsync(new OrganizationUser + { + OrganizationId = organization1.Id, + UserId = user.Id, + Status = OrganizationUserStatusType.Confirmed, + ResetPasswordKey = "resetpasswordkey1", + }); + + await organizationUserRepository.CreateAsync(new OrganizationUser + { + OrganizationId = organization2.Id, + UserId = user.Id, + Status = OrganizationUserStatusType.Confirmed, + ResetPasswordKey = "resetpasswordkey2", + }); + + var result = await organizationRepository.GetByVerifiedUserEmailDomainAsync(user.Id); + + Assert.Equal(2, result.Count); + Assert.Contains(result, org => org.Id == organization1.Id); + Assert.Contains(result, org => org.Id == organization2.Id); + } + + [DatabaseTheory, DatabaseData] + public async Task GetByVerifiedUserEmailDomainAsync_WithNonExistentUser_ReturnsEmpty( + IOrganizationRepository organizationRepository) + { + var nonExistentUserId = Guid.NewGuid(); + + var result = await organizationRepository.GetByVerifiedUserEmailDomainAsync(nonExistentUserId); + + Assert.Empty(result); } }