diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 1b76bccf2..ada906329 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "swashbuckle.aspnetcore.cli": { - "version": "6.8.0", + "version": "6.8.1", "commands": ["swagger"] }, "dotnet-ef": { diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0ed3ee0f7..5121d551c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -26,6 +26,9 @@ util/SqliteMigrations/** @bitwarden/dept-dbops bitwarden_license/src/Sso @bitwarden/team-auth-dev src/Identity @bitwarden/team-auth-dev +# Key Management team +**/KeyManagement @bitwarden/team-key-management-dev + **/SecretsManager @bitwarden/team-secrets-manager-dev **/Tools @bitwarden/team-tools-dev diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fe4063f44..4ba3ec22b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -565,6 +565,39 @@ jobs: tag: 'main' } }) + + trigger-ee-updates: + name: Trigger Ephemeral Environment updates + if: github.ref != 'refs/heads/main' && contains(github.event.pull_request.labels.*.name, 'ephemeral-environment') + runs-on: ubuntu-24.04 + needs: build-docker + steps: + - name: Log in to Azure - CI subscription + uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + with: + creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} + + - name: Retrieve GitHub PAT secrets + id: retrieve-secret-pat + uses: bitwarden/gh-actions/get-keyvault-secrets@main + with: + keyvault: "bitwarden-ci" + secrets: "github-pat-bitwarden-devops-bot-repo-scope" + + - name: Trigger Ephemeral Environment update + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + github-token: ${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }} + script: | + await github.rest.actions.createWorkflowDispatch({ + owner: 'bitwarden', + repo: 'devops', + workflow_id: '_update_ephemeral_tags.yml', + ref: 'main', + inputs: { + ephemeral_env_branch: '${{ github.head_ref }}' + } + }) check-failures: name: Check for failures diff --git a/.github/workflows/repository-management.yml b/.github/workflows/repository-management.yml new file mode 100644 index 000000000..29860b868 --- /dev/null +++ b/.github/workflows/repository-management.yml @@ -0,0 +1,249 @@ +name: Repository management + +on: + workflow_dispatch: + inputs: + branch_to_cut: + default: "rc" + description: "Branch to cut" + options: + - "rc" + - "hotfix-rc" + required: true + type: choice + target_ref: + default: "main" + description: "Branch/Tag to target for cut" + required: true + type: string + version_number_override: + description: "New version override (leave blank for automatic calculation, example: '2024.1.0')" + required: false + type: string + +jobs: + cut_branch: + name: Cut branch + runs-on: ubuntu-22.04 + steps: + - name: Check out target ref + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + with: + ref: ${{ inputs.target_ref }} + + - name: Check if ${{ inputs.branch_to_cut }} branch exists + env: + BRANCH_NAME: ${{ inputs.branch_to_cut }} + run: | + if [[ $(git ls-remote --heads origin $BRANCH_NAME) ]]; then + echo "$BRANCH_NAME already exists! Please delete $BRANCH_NAME before running again." >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + - name: Cut branch + env: + BRANCH_NAME: ${{ inputs.branch_to_cut }} + run: | + git switch --quiet --create $BRANCH_NAME + git push --quiet --set-upstream origin $BRANCH_NAME + + + bump_version: + name: Bump Version + runs-on: ubuntu-22.04 + needs: cut_branch + outputs: + version: ${{ steps.set-final-version-output.outputs.version }} + steps: + - name: Validate version input format + if: ${{ inputs.version_number_override != '' }} + uses: bitwarden/gh-actions/version-check@main + with: + version: ${{ inputs.version_number_override }} + + - name: Check out branch + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + with: + ref: main + + - name: Install xmllint + run: | + sudo apt-get update + sudo apt-get install -y libxml2-utils + + - name: Get current version + id: current-version + run: | + CURRENT_VERSION=$(xmllint -xpath "/Project/PropertyGroup/Version/text()" Directory.Build.props) + echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT + + - name: Verify input version + if: ${{ inputs.version_number_override != '' }} + env: + CURRENT_VERSION: ${{ steps.current-version.outputs.version }} + NEW_VERSION: ${{ inputs.version_number_override }} + run: | + # Error if version has not changed. + if [[ "$NEW_VERSION" == "$CURRENT_VERSION" ]]; then + echo "Specified override version is the same as the current version." >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + # Check if version is newer. + printf '%s\n' "${CURRENT_VERSION}" "${NEW_VERSION}" | sort -C -V + if [ $? -eq 0 ]; then + echo "Version is newer than the current version." + else + echo "Version is older than the current version." >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + - name: Calculate next release version + if: ${{ inputs.version_number_override == '' }} + id: calculate-next-version + uses: bitwarden/gh-actions/version-next@main + with: + version: ${{ steps.current-version.outputs.version }} + + - name: Bump version props - Version Override + if: ${{ inputs.version_number_override != '' }} + id: bump-version-override + uses: bitwarden/gh-actions/version-bump@main + with: + file_path: "Directory.Build.props" + version: ${{ inputs.version_number_override }} + + - name: Bump version props - Automatic Calculation + if: ${{ inputs.version_number_override == '' }} + id: bump-version-automatic + uses: bitwarden/gh-actions/version-bump@main + with: + file_path: "Directory.Build.props" + version: ${{ steps.calculate-next-version.outputs.version }} + + - name: Set final version output + id: set-final-version-output + run: | + if [[ "${{ steps.bump-version-override.outcome }}" = "success" ]]; then + echo "version=${{ inputs.version_number_override }}" >> $GITHUB_OUTPUT + elif [[ "${{ steps.bump-version-automatic.outcome }}" = "success" ]]; then + echo "version=${{ steps.calculate-next-version.outputs.version }}" >> $GITHUB_OUTPUT + fi + + - name: Configure Git + run: | + git config --local user.email "actions@github.com" + git config --local user.name "Github Actions" + + - name: Commit files + run: git commit -m "Bumped version to ${{ steps.set-final-version-output.outputs.version }}" -a + + - name: Push changes + run: | + git pull -pt + git push + + + cherry_pick: + name: Cherry-Pick Commit(s) + runs-on: ubuntu-22.04 + needs: bump_version + steps: + - name: Check out main branch + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + with: + ref: main + + - name: Install xmllint + run: | + sudo apt-get update + sudo apt-get install -y libxml2-utils + + - name: Verify version has been updated + env: + NEW_VERSION: ${{ needs.bump_version.outputs.version }} + run: | + # Wait for version to change. + while : ; do + echo "Waiting for version to be updated..." + git pull --force + CURRENT_VERSION=$(xmllint -xpath "/Project/PropertyGroup/Version/text()" Directory.Build.props) + + # If the versions don't match we continue the loop, otherwise we break out of the loop. + [[ "$NEW_VERSION" != "$CURRENT_VERSION" ]] || break + sleep 10 + done + + - name: Get last version commit(s) + id: get-commits + run: | + git switch main + MAIN_COMMIT=$(git log --reverse --pretty=format:"%H" --max-count=1 Directory.Build.props) + echo "main_commit=$MAIN_COMMIT" >> $GITHUB_OUTPUT + + if [[ $(git ls-remote --heads origin rc) ]]; then + git switch rc + RC_COMMIT=$(git log --reverse --pretty=format:"%H" --max-count=1 Directory.Build.props) + echo "rc_commit=$RC_COMMIT" >> $GITHUB_OUTPUT + + RC_VERSION=$(xmllint -xpath "/Project/PropertyGroup/Version/text()" Directory.Build.props) + echo "rc_version=$RC_VERSION" >> $GITHUB_OUTPUT + fi + + - name: Configure Git + run: | + git config --local user.email "actions@github.com" + git config --local user.name "Github Actions" + + - name: Perform cherry-pick(s) + env: + CUT_BRANCH: ${{ inputs.branch_to_cut }} + MAIN_COMMIT: ${{ steps.get-commits.outputs.main_commit }} + RC_COMMIT: ${{ steps.get-commits.outputs.rc_commit }} + RC_VERSION: ${{ steps.get-commits.outputs.rc_version }} + run: | + # If we are cutting 'hotfix-rc': + if [[ "$CUT_BRANCH" == "hotfix-rc" ]]; then + + # If the 'rc' branch exists: + if [[ $(git ls-remote --heads origin rc) ]]; then + + # Chery-pick from 'rc' into 'hotfix-rc' + git switch hotfix-rc + HOTFIX_RC_VERSION=$(xmllint -xpath "/Project/PropertyGroup/Version/text()" Directory.Build.props) + if [[ "$HOTFIX_RC_VERSION" != "$RC_VERSION" ]]; then + git cherry-pick --strategy-option=theirs -x $RC_COMMIT + git push -u origin hotfix-rc + fi + + # Cherry-pick from 'main' into 'rc' + git switch rc + git cherry-pick --strategy-option=theirs -x $MAIN_COMMIT + git push -u origin rc + + # If the 'rc' branch does not exist: + else + + # Cherry-pick from 'main' into 'hotfix-rc' + git switch hotfix-rc + git cherry-pick --strategy-option=theirs -x $MAIN_COMMIT + git push -u origin hotfix-rc + + fi + + # If we are cutting 'rc': + elif [[ "$CUT_BRANCH" == "rc" ]]; then + + # Cherry-pick from 'main' into 'rc' + git switch rc + git cherry-pick --strategy-option=theirs -x $MAIN_COMMIT + git push -u origin rc + + fi + + + move_future_db_scripts: + name: Move finalization database scripts + needs: cherry_pick + uses: ./.github/workflows/_move_finalization_db_scripts.yml + secrets: inherit diff --git a/.github/workflows/version-bump.yml b/.github/workflows/version-bump.yml deleted file mode 100644 index e1d96ee4d..000000000 --- a/.github/workflows/version-bump.yml +++ /dev/null @@ -1,266 +0,0 @@ ---- -name: Version Bump - -on: - workflow_dispatch: - inputs: - version_number_override: - description: "New version override (leave blank for automatic calculation, example: '2024.1.0')" - required: false - type: string - cut_rc_branch: - description: "Cut RC branch?" - default: true - type: boolean - enable_slack_notification: - description: "Enable Slack notifications for upcoming release?" - default: false - type: boolean - -jobs: - bump_version: - name: Bump Version - runs-on: ubuntu-22.04 - outputs: - version: ${{ steps.set-final-version-output.outputs.version }} - steps: - - name: Validate version input - if: ${{ inputs.version_number_override != '' }} - uses: bitwarden/gh-actions/version-check@main - with: - version: ${{ inputs.version_number_override }} - - - name: Slack Notification Check - run: | - if [[ "${{ inputs.enable_slack_notification }}" == true ]]; then - echo "Slack notifications enabled." - else - echo "Slack notifications disabled." - fi - - - name: Check out branch - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - - - name: Check if RC branch exists - if: ${{ inputs.cut_rc_branch == true }} - run: | - remote_rc_branch_check=$(git ls-remote --heads origin rc | wc -l) - if [[ "${remote_rc_branch_check}" -gt 0 ]]; then - echo "Remote RC branch exists." - echo "Please delete current RC branch before running again." - exit 1 - fi - - - name: Log in to Azure - CI subscription - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 - with: - creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - - - name: Retrieve secrets - id: retrieve-secrets - uses: bitwarden/gh-actions/get-keyvault-secrets@main - with: - keyvault: "bitwarden-ci" - secrets: "github-gpg-private-key, - github-gpg-private-key-passphrase" - - - name: Import GPG key - uses: crazy-max/ghaction-import-gpg@01dd5d3ca463c7f10f7f4f7b4f177225ac661ee4 # v6.1.0 - with: - gpg_private_key: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key }} - passphrase: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key-passphrase }} - git_user_signingkey: true - git_commit_gpgsign: true - - - name: Set up Git - run: | - git config --local user.email "106330231+bitwarden-devops-bot@users.noreply.github.com" - git config --local user.name "bitwarden-devops-bot" - - - name: Create version branch - id: create-branch - run: | - NAME=version_bump_${{ github.ref_name }}_$(date +"%Y-%m-%d") - git switch -c $NAME - echo "name=$NAME" >> $GITHUB_OUTPUT - - - name: Install xmllint - run: | - sudo apt-get update - sudo apt-get install -y libxml2-utils - - - name: Get current version - id: current-version - run: | - CURRENT_VERSION=$(xmllint -xpath "/Project/PropertyGroup/Version/text()" Directory.Build.props) - echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT - - - name: Verify input version - if: ${{ inputs.version_number_override != '' }} - env: - CURRENT_VERSION: ${{ steps.current-version.outputs.version }} - NEW_VERSION: ${{ inputs.version_number_override }} - run: | - # Error if version has not changed. - if [[ "$NEW_VERSION" == "$CURRENT_VERSION" ]]; then - echo "Version has not changed." - exit 1 - fi - - # Check if version is newer. - printf '%s\n' "${CURRENT_VERSION}" "${NEW_VERSION}" | sort -C -V - if [ $? -eq 0 ]; then - echo "Version check successful." - else - echo "Version check failed." - exit 1 - fi - - - name: Calculate next release version - if: ${{ inputs.version_number_override == '' }} - id: calculate-next-version - uses: bitwarden/gh-actions/version-next@main - with: - version: ${{ steps.current-version.outputs.version }} - - - name: Bump version props - Version Override - if: ${{ inputs.version_number_override != '' }} - id: bump-version-override - uses: bitwarden/gh-actions/version-bump@main - with: - file_path: "Directory.Build.props" - version: ${{ inputs.version_number_override }} - - - name: Bump version props - Automatic Calculation - if: ${{ inputs.version_number_override == '' }} - id: bump-version-automatic - uses: bitwarden/gh-actions/version-bump@main - with: - file_path: "Directory.Build.props" - version: ${{ steps.calculate-next-version.outputs.version }} - - - name: Set final version output - id: set-final-version-output - run: | - if [[ "${{ steps.bump-version-override.outcome }}" = "success" ]]; then - echo "version=${{ inputs.version_number_override }}" >> $GITHUB_OUTPUT - elif [[ "${{ steps.bump-version-automatic.outcome }}" = "success" ]]; then - echo "version=${{ steps.calculate-next-version.outputs.version }}" >> $GITHUB_OUTPUT - fi - - - name: Check if version changed - id: version-changed - run: | - if [ -n "$(git status --porcelain)" ]; then - echo "changes_to_commit=TRUE" >> $GITHUB_OUTPUT - else - echo "changes_to_commit=FALSE" >> $GITHUB_OUTPUT - echo "No changes to commit!"; - fi - - - name: Commit files - if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }} - run: git commit -m "Bumped version to ${{ steps.set-final-version-output.outputs.version }}" -a - - - name: Push changes - if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }} - env: - PR_BRANCH: ${{ steps.create-branch.outputs.name }} - run: git push -u origin $PR_BRANCH - - - name: Generate GH App token - uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0 - id: app-token - with: - app-id: ${{ secrets.BW_GHAPP_ID }} - private-key: ${{ secrets.BW_GHAPP_KEY }} - owner: ${{ github.repository_owner }} - - - name: Create version PR - if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }} - id: create-pr - env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} - PR_BRANCH: ${{ steps.create-branch.outputs.name }} - TITLE: "Bump version to ${{ steps.set-final-version-output.outputs.version }}" - run: | - PR_URL=$(gh pr create --title "$TITLE" \ - --base "main" \ - --head "$PR_BRANCH" \ - --label "version update" \ - --label "automated pr" \ - --body " - ## Type of change - - [ ] Bug fix - - [ ] New feature development - - [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc) - - [ ] Build/deploy pipeline (DevOps) - - [X] Other - - ## Objective - Automated version bump to ${{ steps.set-final-version-output.outputs.version }}") - echo "pr_number=${PR_URL##*/}" >> $GITHUB_OUTPUT - - - name: Approve PR - if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }} - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }} - run: gh pr review $PR_NUMBER --approve - - - name: Merge PR - if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }} - env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} - PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }} - run: gh pr merge $PR_NUMBER --squash --auto --delete-branch - - - name: Report upcoming release version to Slack - if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' && inputs.enable_slack_notification == true }} - uses: bitwarden/gh-actions/report-upcoming-release-version@main - with: - version: ${{ steps.set-final-version-output.outputs.version }} - project: ${{ github.repository }} - AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - - cut_rc: - name: Cut RC branch - if: ${{ inputs.cut_rc_branch == true }} - needs: bump_version - runs-on: ubuntu-22.04 - steps: - - name: Check out branch - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - with: - ref: main - - - name: Install xmllint - run: | - sudo apt-get update - sudo apt-get install -y libxml2-utils - - - name: Verify version has been updated - env: - NEW_VERSION: ${{ needs.bump_version.outputs.version }} - run: | - # Wait for version to change. - while : ; do - echo "Waiting for version to be updated..." - git pull --force - CURRENT_VERSION=$(xmllint -xpath "/Project/PropertyGroup/Version/text()" Directory.Build.props) - - # If the versions don't match we continue the loop, otherwise we break out of the loop. - [[ "$NEW_VERSION" != "$CURRENT_VERSION" ]] || break - sleep 10 - done - - - name: Cut RC branch - run: | - git switch --quiet --create rc - git push --quiet --set-upstream origin rc - - move-future-db-scripts: - name: Move finalization database scripts - needs: cut_rc - uses: ./.github/workflows/_move_finalization_db_scripts.yml - secrets: inherit diff --git a/Directory.Build.props b/Directory.Build.props index a7133709b..46a12ea3d 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ net8.0 - 2024.9.2 + 2024.10.0 Bit.$(MSBuildProjectName) enable diff --git a/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs b/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs index 866d18f9a..045fd5059 100644 --- a/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs +++ b/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs @@ -2,6 +2,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.Providers.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Constants; @@ -27,6 +28,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv private readonly IFeatureService _featureService; private readonly IProviderBillingService _providerBillingService; private readonly ISubscriberService _subscriberService; + private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery; public RemoveOrganizationFromProviderCommand( IEventService eventService, @@ -37,7 +39,8 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv IStripeAdapter stripeAdapter, IFeatureService featureService, IProviderBillingService providerBillingService, - ISubscriberService subscriberService) + ISubscriberService subscriberService, + IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery) { _eventService = eventService; _mailService = mailService; @@ -48,6 +51,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv _featureService = featureService; _providerBillingService = providerBillingService; _subscriberService = subscriberService; + _hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery; } public async Task RemoveOrganizationFromProvider( @@ -63,7 +67,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv throw new BadRequestException("Failed to remove organization. Please contact support."); } - if (!await _organizationService.HasConfirmedOwnersExceptAsync( + if (!await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync( providerOrganization.OrganizationId, Array.Empty(), includeProvider: false)) diff --git a/bitwarden_license/src/Scim/Controllers/v2/UsersController.cs b/bitwarden_license/src/Scim/Controllers/v2/UsersController.cs index 1429fc387..5e73505b9 100644 --- a/bitwarden_license/src/Scim/Controllers/v2/UsersController.cs +++ b/bitwarden_license/src/Scim/Controllers/v2/UsersController.cs @@ -17,7 +17,6 @@ namespace Bit.Scim.Controllers.v2; [ExceptionHandlerFilter] public class UsersController : Controller { - private readonly IUserService _userService; private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IOrganizationService _organizationService; private readonly IGetUsersListQuery _getUsersListQuery; @@ -27,7 +26,6 @@ public class UsersController : Controller private readonly ILogger _logger; public UsersController( - IUserService userService, IOrganizationUserRepository organizationUserRepository, IOrganizationService organizationService, IGetUsersListQuery getUsersListQuery, @@ -36,7 +34,6 @@ public class UsersController : Controller IPostUserCommand postUserCommand, ILogger logger) { - _userService = userService; _organizationUserRepository = organizationUserRepository; _organizationService = organizationService; _getUsersListQuery = getUsersListQuery; @@ -98,7 +95,7 @@ public class UsersController : Controller if (model.Active && orgUser.Status == OrganizationUserStatusType.Revoked) { - await _organizationService.RestoreUserAsync(orgUser, EventSystemUser.SCIM, _userService); + await _organizationService.RestoreUserAsync(orgUser, EventSystemUser.SCIM); } else if (!model.Active && orgUser.Status != OrganizationUserStatusType.Revoked) { diff --git a/bitwarden_license/src/Scim/Startup.cs b/bitwarden_license/src/Scim/Startup.cs index 388ba5adc..3fac669ed 100644 --- a/bitwarden_license/src/Scim/Startup.cs +++ b/bitwarden_license/src/Scim/Startup.cs @@ -69,6 +69,7 @@ public class Startup // Services services.AddBaseServices(globalSettings); services.AddDefaultServices(globalSettings); + services.AddDistributedCache(globalSettings); services.AddBillingOperations(); services.TryAddSingleton(); diff --git a/bitwarden_license/src/Scim/Users/PatchUserCommand.cs b/bitwarden_license/src/Scim/Users/PatchUserCommand.cs index 075807a58..f4445354c 100644 --- a/bitwarden_license/src/Scim/Users/PatchUserCommand.cs +++ b/bitwarden_license/src/Scim/Users/PatchUserCommand.cs @@ -9,18 +9,15 @@ namespace Bit.Scim.Users; public class PatchUserCommand : IPatchUserCommand { - private readonly IUserService _userService; private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IOrganizationService _organizationService; private readonly ILogger _logger; public PatchUserCommand( - IUserService userService, IOrganizationUserRepository organizationUserRepository, IOrganizationService organizationService, ILogger logger) { - _userService = userService; _organizationUserRepository = organizationUserRepository; _organizationService = organizationService; _logger = logger; @@ -74,7 +71,7 @@ public class PatchUserCommand : IPatchUserCommand { if (active && orgUser.Status == OrganizationUserStatusType.Revoked) { - await _organizationService.RestoreUserAsync(orgUser, EventSystemUser.SCIM, _userService); + await _organizationService.RestoreUserAsync(orgUser, EventSystemUser.SCIM); return true; } else if (!active && orgUser.Status != OrganizationUserStatusType.Revoked) diff --git a/bitwarden_license/src/Sso/package-lock.json b/bitwarden_license/src/Sso/package-lock.json index 32a54b031..51da30772 100644 --- a/bitwarden_license/src/Sso/package-lock.json +++ b/bitwarden_license/src/Sso/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.0", "license": "-", "dependencies": { - "bootstrap": "5.3.3", + "bootstrap": "4.6.2", "font-awesome": "4.7.0", "jquery": "3.7.1", "popper.js": "1.16.1" @@ -18,9 +18,9 @@ "css-loader": "7.1.2", "expose-loader": "5.0.0", "mini-css-extract-plugin": "2.9.1", - "sass": "1.77.8", - "sass-loader": "16.0.1", - "webpack": "5.94.0", + "sass": "1.79.5", + "sass-loader": "16.0.2", + "webpack": "5.95.0", "webpack-cli": "5.1.4" } }, @@ -98,10 +98,296 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@parcel/watcher": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.4.1.tgz", + "integrity": "sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.4.1", + "@parcel/watcher-darwin-arm64": "2.4.1", + "@parcel/watcher-darwin-x64": "2.4.1", + "@parcel/watcher-freebsd-x64": "2.4.1", + "@parcel/watcher-linux-arm-glibc": "2.4.1", + "@parcel/watcher-linux-arm64-glibc": "2.4.1", + "@parcel/watcher-linux-arm64-musl": "2.4.1", + "@parcel/watcher-linux-x64-glibc": "2.4.1", + "@parcel/watcher-linux-x64-musl": "2.4.1", + "@parcel/watcher-win32-arm64": "2.4.1", + "@parcel/watcher-win32-ia32": "2.4.1", + "@parcel/watcher-win32-x64": "2.4.1" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.4.1.tgz", + "integrity": "sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.4.1.tgz", + "integrity": "sha512-ln41eihm5YXIY043vBrrHfn94SIBlqOWmoROhsMVTSXGh0QahKGy77tfEywQ7v3NywyxBBkGIfrWRHm0hsKtzA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.4.1.tgz", + "integrity": "sha512-yrw81BRLjjtHyDu7J61oPuSoeYWR3lDElcPGJyOvIXmor6DEo7/G2u1o7I38cwlcoBHQFULqF6nesIX3tsEXMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.4.1.tgz", + "integrity": "sha512-TJa3Pex/gX3CWIx/Co8k+ykNdDCLx+TuZj3f3h7eOjgpdKM+Mnix37RYsYU4LHhiYJz3DK5nFCCra81p6g050w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.4.1.tgz", + "integrity": "sha512-4rVYDlsMEYfa537BRXxJ5UF4ddNwnr2/1O4MHM5PjI9cvV2qymvhwZSFgXqbS8YoTk5i/JR0L0JDs69BUn45YA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.4.1.tgz", + "integrity": "sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.4.1.tgz", + "integrity": "sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.4.1.tgz", + "integrity": "sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.4.1.tgz", + "integrity": "sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.4.1.tgz", + "integrity": "sha512-Uq2BPp5GWhrq/lcuItCHoqxjULU1QYEcyjSO5jqqOK8RNFDBQnenMMx4gAl3v8GiWa59E9+uDM7yZ6LxwUIfRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.4.1.tgz", + "integrity": "sha512-maNRit5QQV2kgHFSYwftmPBxiuK5u4DXjbXx7q6eKjq5dsLXZ4FJiVvlcw35QXzk0KrUecJmuVFbj4uV9oYrcw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.4.1.tgz", + "integrity": "sha512-+DvS92F9ezicfswqrvIRM2njcYJbd5mb9CUgtrHCHmvn7pPPa+nMDRu1o1bYYz/l5IB2NVGNJWiH7h1E58IF2A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "dev": true, "license": "MIT" }, @@ -113,13 +399,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.1.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.1.0.tgz", - "integrity": "sha512-AOmuRF0R2/5j1knA3c6G3HOk523Ga+l+ZXltX8SF1+5oqcXijjfTd8fY3XRZqSihEu9XhtQnKYLmkFaoxgsJHw==", + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.13.0" + "undici-types": "~6.19.2" } }, "node_modules/@webassemblyjs/ast": { @@ -415,35 +701,8 @@ "ajv": "^8.8.2" } }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/bootstrap": { - "version": "5.3.3", + "version": "4.6.2", "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.2.tgz", "integrity": "sha512-51Bbp/Uxr9aTuy6ca/8FbFloBUJZLHwnhTcnjIeRn2suQWsWzcuJhGjKDB5eppVte/8oCdOL3VuwxvZDUggwGQ==", "funding": [ @@ -476,9 +735,9 @@ } }, "node_modules/browserslist": { - "version": "4.23.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", - "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.0.tgz", + "integrity": "sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==", "dev": true, "funding": [ { @@ -496,8 +755,8 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001646", - "electron-to-chromium": "^1.5.4", + "caniuse-lite": "^1.0.30001663", + "electron-to-chromium": "^1.5.28", "node-releases": "^2.0.18", "update-browserslist-db": "^1.1.0" }, @@ -516,9 +775,9 @@ "license": "MIT" }, "node_modules/caniuse-lite": { - "version": "1.0.30001651", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz", - "integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==", + "version": "1.0.30001668", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001668.tgz", + "integrity": "sha512-nWLrdxqCdblixUO+27JtGJJE/txpJlyUy5YN1u53wLZkP0emYCo5zgS6QYft7VUYR42LGgi/S5hdLZTrnyIddw==", "dev": true, "funding": [ { @@ -537,28 +796,19 @@ "license": "CC-BY-4.0" }, "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", + "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", "dev": true, "license": "MIT", "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "readdirp": "^4.0.1" }, "engines": { - "node": ">= 8.10.0" + "node": ">= 14.16.0" }, "funding": { "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" } }, "node_modules/chrome-trace-event": { @@ -664,10 +914,23 @@ "node": ">=4" } }, + "node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/electron-to-chromium": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.5.tgz", - "integrity": "sha512-QR7/A7ZkMS8tZuoftC/jfqNkZLQO779SSW3YuZHP4eXpj3EffGLFcB/Xu9AAZQzLccTiCV+EmUo3ha4mQ9wnlA==", + "version": "1.5.36", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.36.tgz", + "integrity": "sha512-HYTX8tKge/VNp6FGO+f/uVDmUkq+cEfcxYhKf15Akc4M5yxt5YmorwlAitKWjWhWQnKcDRBAQKXkhqqXMqcrjw==", "dev": true, "license": "ISC" }, @@ -686,9 +949,9 @@ } }, "node_modules/envinfo": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.13.0.tgz", - "integrity": "sha512-cvcaMr7KqXVh4nyzGTVqTum+gAiL265x5jUWQIDLq//zOGbW+gSW/C+OWLleY/rs9Qole6AZLMXPbtIFQbqu+Q==", + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.14.0.tgz", + "integrity": "sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg==", "dev": true, "license": "MIT", "bin": { @@ -706,9 +969,9 @@ "license": "MIT" }, "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "license": "MIT", "engines": { @@ -804,9 +1067,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.1.tgz", - "integrity": "sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.2.tgz", + "integrity": "sha512-GR6f0hD7XXyNJa25Tb9BuIdN0tdr+0BMi6/CJPH3wJO1JjNG3n/VsSw38AwRdKZABm8lGbPfakLRkYzx2V9row==", "dev": true, "license": "MIT" }, @@ -866,21 +1129,6 @@ "node": ">=0.10.3" } }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -891,19 +1139,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/glob-to-regexp": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", @@ -991,23 +1226,10 @@ "node": ">=10.13.0" } }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/is-core-module": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz", - "integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==", + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1158,6 +1380,20 @@ "dev": true, "license": "MIT" }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -1228,6 +1464,13 @@ "dev": true, "license": "MIT" }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT" + }, "node_modules/node-releases": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", @@ -1235,16 +1478,6 @@ "dev": true, "license": "MIT" }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", @@ -1312,9 +1545,9 @@ "license": "MIT" }, "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", "dev": true, "license": "ISC" }, @@ -1356,9 +1589,9 @@ } }, "node_modules/postcss": { - "version": "8.4.41", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz", - "integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==", + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", "dev": true, "funding": [ { @@ -1377,8 +1610,8 @@ "license": "MIT", "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.1", - "source-map-js": "^1.2.0" + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" @@ -1448,9 +1681,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.1.tgz", - "integrity": "sha512-b4dlw/9V8A71rLIDsSwVmak9z2DuBUB7CA1/wSdelNEzqsjoSPeADTWNO09lpH49Diy3/JIZ2bSPB1dI3LJCHg==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "dev": true, "license": "MIT", "dependencies": { @@ -1489,16 +1722,17 @@ } }, "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", + "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", "dev": true, "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, "engines": { - "node": ">=8.10.0" + "node": ">= 14.16.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, "node_modules/rechoir": { @@ -1587,13 +1821,14 @@ "license": "MIT" }, "node_modules/sass": { - "version": "1.77.8", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.8.tgz", - "integrity": "sha512-4UHg6prsrycW20fqLGPShtEvo/WyHRVRHwOP4DzkUrObWoWI05QBSfzU71TVB7PFaL104TwNaHpjlWXAZbQiNQ==", + "version": "1.79.5", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.79.5.tgz", + "integrity": "sha512-W1h5kp6bdhqFh2tk3DsI771MoEJjvrSY/2ihJRJS4pjIyfJCw0nTsxqhnrUzaLMOJjFchj8rOvraI/YUVjtx5g==", "dev": true, "license": "MIT", "dependencies": { - "chokidar": ">=3.0.0 <4.0.0", + "@parcel/watcher": "^2.4.1", + "chokidar": "^4.0.0", "immutable": "^4.0.0", "source-map-js": ">=0.6.2 <2.0.0" }, @@ -1605,9 +1840,9 @@ } }, "node_modules/sass-loader": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.1.tgz", - "integrity": "sha512-xACl1ToTsKnL9Ce5yYpRxrLj9QUDCnwZNhzpC7tKiFyA8zXsd3Ap+HGVnbCgkdQcm43E+i6oKAWBsvGA6ZoiMw==", + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.2.tgz", + "integrity": "sha512-Ll6iXZ1EYwYT19SqW4mSBb76vSSi8JgzElmzIerhEGgzB5hRjDQIWsPmuk1UrAXkR16KJHqVY0eH+5/uw9Tmfw==", "dev": true, "license": "MIT", "dependencies": { @@ -1735,9 +1970,9 @@ } }, "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -1795,9 +2030,9 @@ } }, "node_modules/terser": { - "version": "5.31.5", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.5.tgz", - "integrity": "sha512-YPmas0L0rE1UyLL/llTWA0SiDOqIcAQYLeUj7cJYzXHlRTAnMSg9pPe4VJ5PlKvTrPQsdVFuiRiwyeNlYgwh2Q==", + "version": "5.34.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.34.1.tgz", + "integrity": "sha512-FsJZ7iZLd/BXkz+4xrRTGJ26o/6VTjQytUk8b8OxkwcD2I+79VPJlz7qss1+zE7h8GNIScFqXcDyJ/KqBYZFVA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -1915,16 +2150,16 @@ } }, "node_modules/undici-types": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.13.0.tgz", - "integrity": "sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==", + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "dev": true, "license": "MIT" }, "node_modules/update-browserslist-db": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", - "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", "dev": true, "funding": [ { @@ -1942,8 +2177,8 @@ ], "license": "MIT", "dependencies": { - "escalade": "^3.1.2", - "picocolors": "^1.0.1" + "escalade": "^3.2.0", + "picocolors": "^1.1.0" }, "bin": { "update-browserslist-db": "cli.js" @@ -1970,9 +2205,9 @@ "license": "MIT" }, "node_modules/watchpack": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", - "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", + "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", "dev": true, "license": "MIT", "dependencies": { @@ -1984,9 +2219,9 @@ } }, "node_modules/webpack": { - "version": "5.94.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", - "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==", + "version": "5.95.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.95.0.tgz", + "integrity": "sha512-2t3XstrKULz41MNMBF+cJ97TyHdyQ8HCt//pqErqDvNjU9YQBnZxIHa11VXsi7F3mb5/aO2tuDxdeTPdU7xu9Q==", "dev": true, "license": "MIT", "dependencies": { diff --git a/bitwarden_license/src/Sso/package.json b/bitwarden_license/src/Sso/package.json index 91dc513e1..fa9d37c1b 100644 --- a/bitwarden_license/src/Sso/package.json +++ b/bitwarden_license/src/Sso/package.json @@ -8,7 +8,7 @@ "build": "webpack" }, "dependencies": { - "bootstrap": "5.3.3", + "bootstrap": "4.6.2", "font-awesome": "4.7.0", "jquery": "3.7.1", "popper.js": "1.16.1" @@ -17,9 +17,9 @@ "css-loader": "7.1.2", "expose-loader": "5.0.0", "mini-css-extract-plugin": "2.9.1", - "sass": "1.77.8", - "sass-loader": "16.0.1", - "webpack": "5.94.0", + "sass": "1.79.5", + "sass-loader": "16.0.2", + "webpack": "5.95.0", "webpack-cli": "5.1.4" } } diff --git a/bitwarden_license/test/Commercial.Core.Test/AdminConsole/ProviderFeatures/RemoveOrganizationFromProviderCommandTests.cs b/bitwarden_license/test/Commercial.Core.Test/AdminConsole/ProviderFeatures/RemoveOrganizationFromProviderCommandTests.cs index 064dae26d..e984259e9 100644 --- a/bitwarden_license/test/Commercial.Core.Test/AdminConsole/ProviderFeatures/RemoveOrganizationFromProviderCommandTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/AdminConsole/ProviderFeatures/RemoveOrganizationFromProviderCommandTests.cs @@ -3,6 +3,7 @@ using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Enums; @@ -75,7 +76,7 @@ public class RemoveOrganizationFromProviderCommandTests { providerOrganization.ProviderId = provider.Id; - sutProvider.GetDependency().HasConfirmedOwnersExceptAsync( + sutProvider.GetDependency().HasConfirmedOwnersExceptAsync( providerOrganization.OrganizationId, [], includeProvider: false) @@ -98,7 +99,7 @@ public class RemoveOrganizationFromProviderCommandTests organization.GatewayCustomerId = null; organization.GatewaySubscriptionId = null; - sutProvider.GetDependency().HasConfirmedOwnersExceptAsync( + sutProvider.GetDependency().HasConfirmedOwnersExceptAsync( providerOrganization.OrganizationId, [], includeProvider: false) @@ -141,7 +142,7 @@ public class RemoveOrganizationFromProviderCommandTests { providerOrganization.ProviderId = provider.Id; - sutProvider.GetDependency().HasConfirmedOwnersExceptAsync( + sutProvider.GetDependency().HasConfirmedOwnersExceptAsync( providerOrganization.OrganizationId, [], includeProvider: false) @@ -208,7 +209,7 @@ public class RemoveOrganizationFromProviderCommandTests var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly); - sutProvider.GetDependency().HasConfirmedOwnersExceptAsync( + sutProvider.GetDependency().HasConfirmedOwnersExceptAsync( providerOrganization.OrganizationId, [], includeProvider: false) diff --git a/bitwarden_license/test/Scim.Test/Users/PatchUserCommandTests.cs b/bitwarden_license/test/Scim.Test/Users/PatchUserCommandTests.cs index 977011b35..6e9c985b8 100644 --- a/bitwarden_license/test/Scim.Test/Users/PatchUserCommandTests.cs +++ b/bitwarden_license/test/Scim.Test/Users/PatchUserCommandTests.cs @@ -43,7 +43,7 @@ public class PatchUserCommandTests await sutProvider.Sut.PatchUserAsync(organizationUser.OrganizationId, organizationUser.Id, scimPatchModel); - await sutProvider.GetDependency().Received(1).RestoreUserAsync(organizationUser, EventSystemUser.SCIM, Arg.Any()); + await sutProvider.GetDependency().Received(1).RestoreUserAsync(organizationUser, EventSystemUser.SCIM); } [Theory] @@ -71,7 +71,7 @@ public class PatchUserCommandTests await sutProvider.Sut.PatchUserAsync(organizationUser.OrganizationId, organizationUser.Id, scimPatchModel); - await sutProvider.GetDependency().Received(1).RestoreUserAsync(organizationUser, EventSystemUser.SCIM, Arg.Any()); + await sutProvider.GetDependency().Received(1).RestoreUserAsync(organizationUser, EventSystemUser.SCIM); } [Theory] @@ -147,7 +147,7 @@ public class PatchUserCommandTests await sutProvider.Sut.PatchUserAsync(organizationUser.OrganizationId, organizationUser.Id, scimPatchModel); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().RestoreUserAsync(default, EventSystemUser.SCIM, default); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().RestoreUserAsync(default, EventSystemUser.SCIM); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().RevokeUserAsync(default, EventSystemUser.SCIM); } diff --git a/dev/.gitignore b/dev/.gitignore index 6134bc261..39b657f45 100644 --- a/dev/.gitignore +++ b/dev/.gitignore @@ -5,7 +5,6 @@ secrets.json # Docker container configurations .env authsources.php -directory.ldif # Development certificates identity_server_dev.crt diff --git a/dev/docker-compose.yml b/dev/docker-compose.yml index fd316f8ea..c02d3c872 100644 --- a/dev/docker-compose.yml +++ b/dev/docker-compose.yml @@ -59,7 +59,7 @@ services: container_name: bw-mysql ports: - "3306:3306" - command: + command: - --default-authentication-plugin=mysql_native_password - --innodb-print-all-deadlocks=ON environment: @@ -84,20 +84,6 @@ services: profiles: - idp - open-ldap: - image: osixia/openldap:1.5.0 - command: --copy-service - environment: - LDAP_ORGANISATION: "Bitwarden" - LDAP_DOMAIN: "bitwarden.com" - volumes: - - ./directory.ldif:/container/service/slapd/assets/config/bootstrap/ldif/output.ldif - ports: - - "389:389" - - "636:636" - profiles: - - ldap - reverse-proxy: image: nginx:alpine container_name: reverse-proxy diff --git a/src/Admin/AdminConsole/Controllers/OrganizationsController.cs b/src/Admin/AdminConsole/Controllers/OrganizationsController.cs index 4dc7ec56d..70c09a539 100644 --- a/src/Admin/AdminConsole/Controllers/OrganizationsController.cs +++ b/src/Admin/AdminConsole/Controllers/OrganizationsController.cs @@ -7,7 +7,6 @@ using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Providers.Interfaces; using Bit.Core.AdminConsole.Repositories; -using Bit.Core.AdminConsole.Services; using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Services; using Bit.Core.Context; @@ -57,7 +56,6 @@ public class OrganizationsController : Controller private readonly IRemoveOrganizationFromProviderCommand _removeOrganizationFromProviderCommand; private readonly IFeatureService _featureService; private readonly IProviderBillingService _providerBillingService; - private readonly IPolicyService _policyService; public OrganizationsController( IOrganizationService organizationService, @@ -84,8 +82,7 @@ public class OrganizationsController : Controller IProviderOrganizationRepository providerOrganizationRepository, IRemoveOrganizationFromProviderCommand removeOrganizationFromProviderCommand, IFeatureService featureService, - IProviderBillingService providerBillingService, - IPolicyService policyService) + IProviderBillingService providerBillingService) { _organizationService = organizationService; _organizationRepository = organizationRepository; @@ -112,7 +109,6 @@ public class OrganizationsController : Controller _removeOrganizationFromProviderCommand = removeOrganizationFromProviderCommand; _featureService = featureService; _providerBillingService = providerBillingService; - _policyService = policyService; } [RequirePermission(Permission.Org_List_View)] @@ -440,13 +436,6 @@ public class OrganizationsController : Controller organization.MaxAutoscaleSmServiceAccounts = model.MaxAutoscaleSmServiceAccounts; } - var plan = StaticStore.GetPlan(organization.PlanType); - - if (!organization.UsePolicies || !plan.HasPolicies) - { - await DisableOrganizationPoliciesAsync(organization.Id); - } - if (_accessControlService.UserHasPermission(Permission.Org_Licensing_Edit)) { organization.LicenseKey = model.LicenseKey; @@ -463,18 +452,4 @@ public class OrganizationsController : Controller return organization; } - - private async Task DisableOrganizationPoliciesAsync(Guid organizationId) - { - var policies = await _policyRepository.GetManyByOrganizationIdAsync(organizationId); - - if (policies.Count != 0) - { - await Task.WhenAll(policies.Select(async policy => - { - policy.Enabled = false; - await _policyService.SaveAsync(policy, _userService, _organizationService, null); - })); - } - } } diff --git a/src/Admin/Controllers/UsersController.cs b/src/Admin/Controllers/UsersController.cs index 842abaea6..2842efcdb 100644 --- a/src/Admin/Controllers/UsersController.cs +++ b/src/Admin/Controllers/UsersController.cs @@ -4,7 +4,6 @@ using Bit.Admin.Enums; using Bit.Admin.Models; using Bit.Admin.Services; using Bit.Admin.Utilities; -using Bit.Core; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; @@ -25,8 +24,6 @@ public class UsersController : Controller private readonly GlobalSettings _globalSettings; private readonly IAccessControlService _accessControlService; private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; - private readonly IFeatureService _featureService; - private readonly IUserService _userService; public UsersController( IUserRepository userRepository, @@ -34,9 +31,7 @@ public class UsersController : Controller IPaymentService paymentService, GlobalSettings globalSettings, IAccessControlService accessControlService, - ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, - IFeatureService featureService, - IUserService userService) + ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery) { _userRepository = userRepository; _cipherRepository = cipherRepository; @@ -44,8 +39,6 @@ public class UsersController : Controller _globalSettings = globalSettings; _accessControlService = accessControlService; _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; - _featureService = featureService; - _userService = userService; } [RequirePermission(Permission.User_List_View)] @@ -64,22 +57,8 @@ public class UsersController : Controller var skip = (page - 1) * count; var users = await _userRepository.SearchAsync(email, skip, count); - var userModels = new List(); - - if (_featureService.IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization)) - { - var twoFactorAuthLookup = (await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(users.Select(u => u.Id))).ToList(); - - userModels = UserViewModel.MapViewModels(users, twoFactorAuthLookup).ToList(); - } - else - { - foreach (var user in users) - { - var isTwoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user); - userModels.Add(UserViewModel.MapViewModel(user, isTwoFactorEnabled)); - } - } + var twoFactorAuthLookup = (await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(users.Select(u => u.Id))).ToList(); + var userModels = UserViewModel.MapViewModels(users, twoFactorAuthLookup).ToList(); return View(new UsersModel { diff --git a/src/Admin/package-lock.json b/src/Admin/package-lock.json index 716c4d272..e6ed279ed 100644 --- a/src/Admin/package-lock.json +++ b/src/Admin/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.0", "license": "GPL-3.0", "dependencies": { - "bootstrap": "5.3.3", + "bootstrap": "4.6.2", "font-awesome": "4.7.0", "jquery": "3.7.1", "popper.js": "1.16.1", @@ -19,9 +19,9 @@ "css-loader": "7.1.2", "expose-loader": "5.0.0", "mini-css-extract-plugin": "2.9.1", - "sass": "1.77.8", - "sass-loader": "16.0.1", - "webpack": "5.94.0", + "sass": "1.79.5", + "sass-loader": "16.0.2", + "webpack": "5.95.0", "webpack-cli": "5.1.4" } }, @@ -99,10 +99,296 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@parcel/watcher": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.4.1.tgz", + "integrity": "sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.4.1", + "@parcel/watcher-darwin-arm64": "2.4.1", + "@parcel/watcher-darwin-x64": "2.4.1", + "@parcel/watcher-freebsd-x64": "2.4.1", + "@parcel/watcher-linux-arm-glibc": "2.4.1", + "@parcel/watcher-linux-arm64-glibc": "2.4.1", + "@parcel/watcher-linux-arm64-musl": "2.4.1", + "@parcel/watcher-linux-x64-glibc": "2.4.1", + "@parcel/watcher-linux-x64-musl": "2.4.1", + "@parcel/watcher-win32-arm64": "2.4.1", + "@parcel/watcher-win32-ia32": "2.4.1", + "@parcel/watcher-win32-x64": "2.4.1" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.4.1.tgz", + "integrity": "sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.4.1.tgz", + "integrity": "sha512-ln41eihm5YXIY043vBrrHfn94SIBlqOWmoROhsMVTSXGh0QahKGy77tfEywQ7v3NywyxBBkGIfrWRHm0hsKtzA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.4.1.tgz", + "integrity": "sha512-yrw81BRLjjtHyDu7J61oPuSoeYWR3lDElcPGJyOvIXmor6DEo7/G2u1o7I38cwlcoBHQFULqF6nesIX3tsEXMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.4.1.tgz", + "integrity": "sha512-TJa3Pex/gX3CWIx/Co8k+ykNdDCLx+TuZj3f3h7eOjgpdKM+Mnix37RYsYU4LHhiYJz3DK5nFCCra81p6g050w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.4.1.tgz", + "integrity": "sha512-4rVYDlsMEYfa537BRXxJ5UF4ddNwnr2/1O4MHM5PjI9cvV2qymvhwZSFgXqbS8YoTk5i/JR0L0JDs69BUn45YA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.4.1.tgz", + "integrity": "sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.4.1.tgz", + "integrity": "sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.4.1.tgz", + "integrity": "sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.4.1.tgz", + "integrity": "sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.4.1.tgz", + "integrity": "sha512-Uq2BPp5GWhrq/lcuItCHoqxjULU1QYEcyjSO5jqqOK8RNFDBQnenMMx4gAl3v8GiWa59E9+uDM7yZ6LxwUIfRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.4.1.tgz", + "integrity": "sha512-maNRit5QQV2kgHFSYwftmPBxiuK5u4DXjbXx7q6eKjq5dsLXZ4FJiVvlcw35QXzk0KrUecJmuVFbj4uV9oYrcw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.4.1.tgz", + "integrity": "sha512-+DvS92F9ezicfswqrvIRM2njcYJbd5mb9CUgtrHCHmvn7pPPa+nMDRu1o1bYYz/l5IB2NVGNJWiH7h1E58IF2A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "dev": true, "license": "MIT" }, @@ -114,13 +400,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.1.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.1.0.tgz", - "integrity": "sha512-AOmuRF0R2/5j1knA3c6G3HOk523Ga+l+ZXltX8SF1+5oqcXijjfTd8fY3XRZqSihEu9XhtQnKYLmkFaoxgsJHw==", + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.13.0" + "undici-types": "~6.19.2" } }, "node_modules/@webassemblyjs/ast": { @@ -416,35 +702,8 @@ "ajv": "^8.8.2" } }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/bootstrap": { - "version": "5.3.3", + "version": "4.6.2", "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.2.tgz", "integrity": "sha512-51Bbp/Uxr9aTuy6ca/8FbFloBUJZLHwnhTcnjIeRn2suQWsWzcuJhGjKDB5eppVte/8oCdOL3VuwxvZDUggwGQ==", "funding": [ @@ -477,9 +736,9 @@ } }, "node_modules/browserslist": { - "version": "4.23.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", - "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.0.tgz", + "integrity": "sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==", "dev": true, "funding": [ { @@ -497,8 +756,8 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001646", - "electron-to-chromium": "^1.5.4", + "caniuse-lite": "^1.0.30001663", + "electron-to-chromium": "^1.5.28", "node-releases": "^2.0.18", "update-browserslist-db": "^1.1.0" }, @@ -517,9 +776,9 @@ "license": "MIT" }, "node_modules/caniuse-lite": { - "version": "1.0.30001651", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz", - "integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==", + "version": "1.0.30001668", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001668.tgz", + "integrity": "sha512-nWLrdxqCdblixUO+27JtGJJE/txpJlyUy5YN1u53wLZkP0emYCo5zgS6QYft7VUYR42LGgi/S5hdLZTrnyIddw==", "dev": true, "funding": [ { @@ -538,28 +797,19 @@ "license": "CC-BY-4.0" }, "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", + "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", "dev": true, "license": "MIT", "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "readdirp": "^4.0.1" }, "engines": { - "node": ">= 8.10.0" + "node": ">= 14.16.0" }, "funding": { "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" } }, "node_modules/chrome-trace-event": { @@ -665,10 +915,23 @@ "node": ">=4" } }, + "node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/electron-to-chromium": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.5.tgz", - "integrity": "sha512-QR7/A7ZkMS8tZuoftC/jfqNkZLQO779SSW3YuZHP4eXpj3EffGLFcB/Xu9AAZQzLccTiCV+EmUo3ha4mQ9wnlA==", + "version": "1.5.36", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.36.tgz", + "integrity": "sha512-HYTX8tKge/VNp6FGO+f/uVDmUkq+cEfcxYhKf15Akc4M5yxt5YmorwlAitKWjWhWQnKcDRBAQKXkhqqXMqcrjw==", "dev": true, "license": "ISC" }, @@ -687,9 +950,9 @@ } }, "node_modules/envinfo": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.13.0.tgz", - "integrity": "sha512-cvcaMr7KqXVh4nyzGTVqTum+gAiL265x5jUWQIDLq//zOGbW+gSW/C+OWLleY/rs9Qole6AZLMXPbtIFQbqu+Q==", + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.14.0.tgz", + "integrity": "sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg==", "dev": true, "license": "MIT", "bin": { @@ -707,9 +970,9 @@ "license": "MIT" }, "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "license": "MIT", "engines": { @@ -805,9 +1068,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.1.tgz", - "integrity": "sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.2.tgz", + "integrity": "sha512-GR6f0hD7XXyNJa25Tb9BuIdN0tdr+0BMi6/CJPH3wJO1JjNG3n/VsSw38AwRdKZABm8lGbPfakLRkYzx2V9row==", "dev": true, "license": "MIT" }, @@ -867,21 +1130,6 @@ "node": ">=0.10.3" } }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -892,19 +1140,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/glob-to-regexp": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", @@ -992,23 +1227,10 @@ "node": ">=10.13.0" } }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/is-core-module": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz", - "integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==", + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1159,6 +1381,20 @@ "dev": true, "license": "MIT" }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -1229,6 +1465,13 @@ "dev": true, "license": "MIT" }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT" + }, "node_modules/node-releases": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", @@ -1236,16 +1479,6 @@ "dev": true, "license": "MIT" }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", @@ -1313,9 +1546,9 @@ "license": "MIT" }, "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", "dev": true, "license": "ISC" }, @@ -1357,9 +1590,9 @@ } }, "node_modules/postcss": { - "version": "8.4.41", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz", - "integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==", + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", "dev": true, "funding": [ { @@ -1378,8 +1611,8 @@ "license": "MIT", "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.1", - "source-map-js": "^1.2.0" + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" @@ -1449,9 +1682,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.1.tgz", - "integrity": "sha512-b4dlw/9V8A71rLIDsSwVmak9z2DuBUB7CA1/wSdelNEzqsjoSPeADTWNO09lpH49Diy3/JIZ2bSPB1dI3LJCHg==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "dev": true, "license": "MIT", "dependencies": { @@ -1490,16 +1723,17 @@ } }, "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", + "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", "dev": true, "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, "engines": { - "node": ">=8.10.0" + "node": ">= 14.16.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, "node_modules/rechoir": { @@ -1588,13 +1822,14 @@ "license": "MIT" }, "node_modules/sass": { - "version": "1.77.8", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.8.tgz", - "integrity": "sha512-4UHg6prsrycW20fqLGPShtEvo/WyHRVRHwOP4DzkUrObWoWI05QBSfzU71TVB7PFaL104TwNaHpjlWXAZbQiNQ==", + "version": "1.79.5", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.79.5.tgz", + "integrity": "sha512-W1h5kp6bdhqFh2tk3DsI771MoEJjvrSY/2ihJRJS4pjIyfJCw0nTsxqhnrUzaLMOJjFchj8rOvraI/YUVjtx5g==", "dev": true, "license": "MIT", "dependencies": { - "chokidar": ">=3.0.0 <4.0.0", + "@parcel/watcher": "^2.4.1", + "chokidar": "^4.0.0", "immutable": "^4.0.0", "source-map-js": ">=0.6.2 <2.0.0" }, @@ -1606,9 +1841,9 @@ } }, "node_modules/sass-loader": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.1.tgz", - "integrity": "sha512-xACl1ToTsKnL9Ce5yYpRxrLj9QUDCnwZNhzpC7tKiFyA8zXsd3Ap+HGVnbCgkdQcm43E+i6oKAWBsvGA6ZoiMw==", + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.2.tgz", + "integrity": "sha512-Ll6iXZ1EYwYT19SqW4mSBb76vSSi8JgzElmzIerhEGgzB5hRjDQIWsPmuk1UrAXkR16KJHqVY0eH+5/uw9Tmfw==", "dev": true, "license": "MIT", "dependencies": { @@ -1736,9 +1971,9 @@ } }, "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -1796,9 +2031,9 @@ } }, "node_modules/terser": { - "version": "5.31.5", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.5.tgz", - "integrity": "sha512-YPmas0L0rE1UyLL/llTWA0SiDOqIcAQYLeUj7cJYzXHlRTAnMSg9pPe4VJ5PlKvTrPQsdVFuiRiwyeNlYgwh2Q==", + "version": "5.34.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.34.1.tgz", + "integrity": "sha512-FsJZ7iZLd/BXkz+4xrRTGJ26o/6VTjQytUk8b8OxkwcD2I+79VPJlz7qss1+zE7h8GNIScFqXcDyJ/KqBYZFVA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -1924,16 +2159,16 @@ } }, "node_modules/undici-types": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.13.0.tgz", - "integrity": "sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==", + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "dev": true, "license": "MIT" }, "node_modules/update-browserslist-db": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", - "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", "dev": true, "funding": [ { @@ -1951,8 +2186,8 @@ ], "license": "MIT", "dependencies": { - "escalade": "^3.1.2", - "picocolors": "^1.0.1" + "escalade": "^3.2.0", + "picocolors": "^1.1.0" }, "bin": { "update-browserslist-db": "cli.js" @@ -1979,9 +2214,9 @@ "license": "MIT" }, "node_modules/watchpack": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", - "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", + "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", "dev": true, "license": "MIT", "dependencies": { @@ -1993,9 +2228,9 @@ } }, "node_modules/webpack": { - "version": "5.94.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", - "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==", + "version": "5.95.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.95.0.tgz", + "integrity": "sha512-2t3XstrKULz41MNMBF+cJ97TyHdyQ8HCt//pqErqDvNjU9YQBnZxIHa11VXsi7F3mb5/aO2tuDxdeTPdU7xu9Q==", "dev": true, "license": "MIT", "dependencies": { diff --git a/src/Admin/package.json b/src/Admin/package.json index 1196b2453..ac78f8935 100644 --- a/src/Admin/package.json +++ b/src/Admin/package.json @@ -8,7 +8,7 @@ "build": "webpack" }, "dependencies": { - "bootstrap": "5.3.3", + "bootstrap": "4.6.2", "font-awesome": "4.7.0", "jquery": "3.7.1", "popper.js": "1.16.1", @@ -18,9 +18,9 @@ "css-loader": "7.1.2", "expose-loader": "5.0.0", "mini-css-extract-plugin": "2.9.1", - "sass": "1.77.8", - "sass-loader": "16.0.1", - "webpack": "5.94.0", + "sass": "1.79.5", + "sass-loader": "16.0.2", + "webpack": "5.95.0", "webpack-cli": "5.1.4" } } diff --git a/src/Api/AdminConsole/Controllers/OrganizationDomainController.cs b/src/Api/AdminConsole/Controllers/OrganizationDomainController.cs index 35c927d5a..af7a162d8 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationDomainController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationDomainController.cs @@ -2,11 +2,13 @@ using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Api.Models.Response; +using Bit.Core; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.Repositories; +using Bit.Core.Utilities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -133,6 +135,20 @@ public class OrganizationDomainController : Controller return new OrganizationDomainSsoDetailsResponseModel(ssoResult); } + [AllowAnonymous] + [HttpPost("domain/sso/verified")] + [RequireFeature(FeatureFlagKeys.VerifiedSsoDomainEndpoint)] + public async Task GetVerifiedOrgDomainSsoDetailsAsync( + [FromBody] OrganizationDomainSsoDetailsRequestModel model) + { + var ssoResults = (await _organizationDomainRepository + .GetVerifiedOrganizationDomainSsoDetailsAsync(model.Email)) + .ToList(); + + return new VerifiedOrganizationDomainSsoDetailsResponseModel( + ssoResults.Select(ssoResult => new VerifiedOrganizationDomainSsoDetailResponseModel(ssoResult))); + } + private async Task ValidateOrganizationAccessAsync(Guid orgIdGuid) { if (!await _currentContext.ManageSso(orgIdGuid)) diff --git a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs index cd6bdd6fa..af0aede5a 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs @@ -48,13 +48,12 @@ public class OrganizationUsersController : Controller private readonly IAcceptOrgUserCommand _acceptOrgUserCommand; private readonly IAuthorizationService _authorizationService; private readonly IApplicationCacheService _applicationCacheService; - private readonly IFeatureService _featureService; private readonly ISsoConfigRepository _ssoConfigRepository; private readonly IOrganizationUserUserDetailsQuery _organizationUserUserDetailsQuery; private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; + private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; private readonly IDeleteManagedOrganizationUserAccountCommand _deleteManagedOrganizationUserAccountCommand; - public OrganizationUsersController( IOrganizationRepository organizationRepository, IOrganizationUserRepository organizationUserRepository, @@ -70,10 +69,10 @@ public class OrganizationUsersController : Controller IAcceptOrgUserCommand acceptOrgUserCommand, IAuthorizationService authorizationService, IApplicationCacheService applicationCacheService, - IFeatureService featureService, ISsoConfigRepository ssoConfigRepository, IOrganizationUserUserDetailsQuery organizationUserUserDetailsQuery, ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, + IRemoveOrganizationUserCommand removeOrganizationUserCommand, IDeleteManagedOrganizationUserAccountCommand deleteManagedOrganizationUserAccountCommand) { _organizationRepository = organizationRepository; @@ -90,10 +89,10 @@ public class OrganizationUsersController : Controller _acceptOrgUserCommand = acceptOrgUserCommand; _authorizationService = authorizationService; _applicationCacheService = applicationCacheService; - _featureService = featureService; _ssoConfigRepository = ssoConfigRepository; _organizationUserUserDetailsQuery = organizationUserUserDetailsQuery; _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; + _removeOrganizationUserCommand = removeOrganizationUserCommand; _deleteManagedOrganizationUserAccountCommand = deleteManagedOrganizationUserAccountCommand; } @@ -142,11 +141,6 @@ public class OrganizationUsersController : Controller throw new NotFoundException(); } - if (_featureService.IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization)) - { - return await Get_vNext(orgId, includeGroups, includeCollections); - } - var organizationUsers = await _organizationUserUserDetailsQuery.GetOrganizationUserUserDetails( new OrganizationUserUserDetailsQueryRequest { @@ -155,17 +149,15 @@ public class OrganizationUsersController : Controller IncludeCollections = includeCollections } ); - - var responseTasks = organizationUsers - .Select(async o => + var organizationUsersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(organizationUsers); + var responses = organizationUsers + .Select(o => { - var orgUser = new OrganizationUserUserDetailsResponseModel(o, - await _userService.TwoFactorIsEnabledAsync(o)); + var userTwoFactorEnabled = organizationUsersTwoFactorEnabled.FirstOrDefault(u => u.user.Id == o.Id).twoFactorIsEnabled; + var orgUser = new OrganizationUserUserDetailsResponseModel(o, userTwoFactorEnabled); return orgUser; }); - var responses = await Task.WhenAll(responseTasks); - return new ListResponseModel(responses); } @@ -296,7 +288,7 @@ public class OrganizationUsersController : Controller await _organizationService.InitPendingOrganization(user.Id, orgId, organizationUserId, model.Keys.PublicKey, model.Keys.EncryptedPrivateKey, model.CollectionName); await _acceptOrgUserCommand.AcceptOrgUserByEmailTokenAsync(organizationUserId, user, model.Token, _userService); - await _organizationService.ConfirmUserAsync(orgId, organizationUserId, model.Key, user.Id, _userService); + await _organizationService.ConfirmUserAsync(orgId, organizationUserId, model.Key, user.Id); } [HttpPost("{organizationUserId}/accept")] @@ -335,8 +327,7 @@ public class OrganizationUsersController : Controller } var userId = _userService.GetProperUserId(User); - var result = await _organizationService.ConfirmUserAsync(orgGuidId, new Guid(id), model.Key, userId.Value, - _userService); + var result = await _organizationService.ConfirmUserAsync(orgGuidId, new Guid(id), model.Key, userId.Value); } [HttpPost("confirm")] @@ -350,10 +341,7 @@ public class OrganizationUsersController : Controller } var userId = _userService.GetProperUserId(User); - var results = _featureService.IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization) - ? await _organizationService.ConfirmUsersAsync_vNext(orgGuidId, model.ToDictionary(), userId.Value) - : await _organizationService.ConfirmUsersAsync(orgGuidId, model.ToDictionary(), userId.Value, - _userService); + var results = await _organizationService.ConfirmUsersAsync(orgGuidId, model.ToDictionary(), userId.Value); return new ListResponseModel(results.Select(r => new OrganizationUserBulkResponseModel(r.Item1.Id, r.Item2))); @@ -517,30 +505,28 @@ public class OrganizationUsersController : Controller [HttpDelete("{id}")] [HttpPost("{id}/remove")] - public async Task Remove(string orgId, string id) + public async Task Remove(Guid orgId, Guid id) { - var orgGuidId = new Guid(orgId); - if (!await _currentContext.ManageUsers(orgGuidId)) + if (!await _currentContext.ManageUsers(orgId)) { throw new NotFoundException(); } var userId = _userService.GetProperUserId(User); - await _organizationService.RemoveUserAsync(orgGuidId, new Guid(id), userId.Value); + await _removeOrganizationUserCommand.RemoveUserAsync(orgId, id, userId.Value); } [HttpDelete("")] [HttpPost("remove")] - public async Task> BulkRemove(string orgId, [FromBody] OrganizationUserBulkRequestModel model) + public async Task> BulkRemove(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model) { - var orgGuidId = new Guid(orgId); - if (!await _currentContext.ManageUsers(orgGuidId)) + if (!await _currentContext.ManageUsers(orgId)) { throw new NotFoundException(); } var userId = _userService.GetProperUserId(User); - var result = await _organizationService.RemoveUsersAsync(orgGuidId, model.Ids, userId.Value); + var result = await _removeOrganizationUserCommand.RemoveUsersAsync(orgId, model.Ids, userId.Value); return new ListResponseModel(result.Select(r => new OrganizationUserBulkResponseModel(r.Item1.Id, r.Item2))); } @@ -616,7 +602,7 @@ public class OrganizationUsersController : Controller [HttpPut("{id}/restore")] public async Task RestoreAsync(Guid orgId, Guid id) { - await RestoreOrRevokeUserAsync(orgId, id, (orgUser, userId) => _organizationService.RestoreUserAsync(orgUser, userId, _userService)); + await RestoreOrRevokeUserAsync(orgId, id, (orgUser, userId) => _organizationService.RestoreUserAsync(orgUser, userId)); } [HttpPatch("restore")] @@ -696,27 +682,4 @@ public class OrganizationUsersController : Controller return new ListResponseModel(result.Select(r => new OrganizationUserBulkResponseModel(r.Item1.Id, r.Item2))); } - - private async Task> Get_vNext(Guid orgId, - bool includeGroups = false, bool includeCollections = false) - { - var organizationUsers = await _organizationUserUserDetailsQuery.GetOrganizationUserUserDetails( - new OrganizationUserUserDetailsQueryRequest - { - OrganizationId = orgId, - IncludeGroups = includeGroups, - IncludeCollections = includeCollections - } - ); - var organizationUsersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(organizationUsers); - var responses = organizationUsers - .Select(o => - { - var userTwoFactorEnabled = organizationUsersTwoFactorEnabled.FirstOrDefault(u => u.user.Id == o.Id).twoFactorIsEnabled; - var orgUser = new OrganizationUserUserDetailsResponseModel(o, userTwoFactorEnabled); - - return orgUser; - }); - return new ListResponseModel(responses); - } } diff --git a/src/Api/AdminConsole/Controllers/OrganizationsController.cs b/src/Api/AdminConsole/Controllers/OrganizationsController.cs index e5dbcd10b..6bcf75b35 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationsController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationsController.cs @@ -13,6 +13,7 @@ using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Business.Tokenables; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Repositories; @@ -55,6 +56,7 @@ public class OrganizationsController : Controller private readonly IProviderRepository _providerRepository; private readonly IProviderBillingService _providerBillingService; private readonly IDataProtectorTokenFactory _orgDeleteTokenDataFactory; + private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; public OrganizationsController( IOrganizationRepository organizationRepository, @@ -74,7 +76,8 @@ public class OrganizationsController : Controller IPushNotificationService pushNotificationService, IProviderRepository providerRepository, IProviderBillingService providerBillingService, - IDataProtectorTokenFactory orgDeleteTokenDataFactory) + IDataProtectorTokenFactory orgDeleteTokenDataFactory, + IRemoveOrganizationUserCommand removeOrganizationUserCommand) { _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; @@ -94,6 +97,7 @@ public class OrganizationsController : Controller _providerRepository = providerRepository; _providerBillingService = providerBillingService; _orgDeleteTokenDataFactory = orgDeleteTokenDataFactory; + _removeOrganizationUserCommand = removeOrganizationUserCommand; } [HttpGet("{id}")] @@ -229,24 +233,22 @@ public class OrganizationsController : Controller } [HttpPost("{id}/leave")] - public async Task Leave(string id) + public async Task Leave(Guid id) { - var orgGuidId = new Guid(id); - if (!await _currentContext.OrganizationUser(orgGuidId)) + if (!await _currentContext.OrganizationUser(id)) { throw new NotFoundException(); } var user = await _userService.GetUserByPrincipalAsync(User); - var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(orgGuidId); + var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(id); if (ssoConfig?.GetData()?.MemberDecryptionType == MemberDecryptionType.KeyConnector && user.UsesKeyConnector) { throw new BadRequestException("Your organization's Single Sign-On settings prevent you from leaving."); } - - await _organizationService.RemoveUserAsync(orgGuidId, user.Id); + await _removeOrganizationUserCommand.RemoveUserAsync(id, user.Id); } [HttpDelete("{id}")] diff --git a/src/Api/AdminConsole/Controllers/PoliciesController.cs b/src/Api/AdminConsole/Controllers/PoliciesController.cs index 5668031d2..b3be852db 100644 --- a/src/Api/AdminConsole/Controllers/PoliciesController.cs +++ b/src/Api/AdminConsole/Controllers/PoliciesController.cs @@ -185,7 +185,7 @@ public class PoliciesController : Controller } var userId = _userService.GetProperUserId(User); - await _policyService.SaveAsync(policy, _userService, _organizationService, userId); + await _policyService.SaveAsync(policy, _organizationService, userId); return new PolicyResponseModel(policy); } } diff --git a/src/Api/AdminConsole/Models/Response/Organizations/VerifiedOrganizationDomainSsoDetailResponseModel.cs b/src/Api/AdminConsole/Models/Response/Organizations/VerifiedOrganizationDomainSsoDetailResponseModel.cs new file mode 100644 index 000000000..be4d8865d --- /dev/null +++ b/src/Api/AdminConsole/Models/Response/Organizations/VerifiedOrganizationDomainSsoDetailResponseModel.cs @@ -0,0 +1,23 @@ +using Bit.Core.Models.Api; +using Bit.Core.Models.Data.Organizations; + +namespace Bit.Api.AdminConsole.Models.Response.Organizations; + +public class VerifiedOrganizationDomainSsoDetailResponseModel : ResponseModel +{ + public VerifiedOrganizationDomainSsoDetailResponseModel(VerifiedOrganizationDomainSsoDetail data) + : base("verifiedOrganizationDomainSsoDetails") + { + if (data is null) + { + throw new ArgumentNullException(nameof(data)); + } + + DomainName = data.DomainName; + OrganizationIdentifier = data.OrganizationIdentifier; + OrganizationName = data.OrganizationName; + } + public string DomainName { get; } + public string OrganizationIdentifier { get; } + public string OrganizationName { get; } +} diff --git a/src/Api/AdminConsole/Models/Response/Organizations/VerifiedOrganizationDomainSsoDetailsResponseModel.cs b/src/Api/AdminConsole/Models/Response/Organizations/VerifiedOrganizationDomainSsoDetailsResponseModel.cs new file mode 100644 index 000000000..3488eab2c --- /dev/null +++ b/src/Api/AdminConsole/Models/Response/Organizations/VerifiedOrganizationDomainSsoDetailsResponseModel.cs @@ -0,0 +1,8 @@ +using Bit.Api.Models.Response; + +namespace Bit.Api.AdminConsole.Models.Response.Organizations; + +public class VerifiedOrganizationDomainSsoDetailsResponseModel( + IEnumerable data, + string continuationToken = null) + : ListResponseModel(data, continuationToken); diff --git a/src/Api/AdminConsole/Public/Controllers/MembersController.cs b/src/Api/AdminConsole/Public/Controllers/MembersController.cs index a737f0b49..7515111d2 100644 --- a/src/Api/AdminConsole/Public/Controllers/MembersController.cs +++ b/src/Api/AdminConsole/Public/Controllers/MembersController.cs @@ -2,12 +2,10 @@ using Bit.Api.AdminConsole.Public.Models.Request; using Bit.Api.AdminConsole.Public.Models.Response; using Bit.Api.Models.Public.Response; -using Bit.Core; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Context; -using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; using Bit.Core.Services; using Microsoft.AspNetCore.Authorization; @@ -29,8 +27,8 @@ public class MembersController : Controller private readonly IApplicationCacheService _applicationCacheService; private readonly IPaymentService _paymentService; private readonly IOrganizationRepository _organizationRepository; - private readonly IFeatureService _featureService; private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; + private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; public MembersController( IOrganizationUserRepository organizationUserRepository, @@ -43,8 +41,8 @@ public class MembersController : Controller IApplicationCacheService applicationCacheService, IPaymentService paymentService, IOrganizationRepository organizationRepository, - IFeatureService featureService, - ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery) + ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, + IRemoveOrganizationUserCommand removeOrganizationUserCommand) { _organizationUserRepository = organizationUserRepository; _groupRepository = groupRepository; @@ -56,8 +54,8 @@ public class MembersController : Controller _applicationCacheService = applicationCacheService; _paymentService = paymentService; _organizationRepository = organizationRepository; - _featureService = featureService; _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; + _removeOrganizationUserCommand = removeOrganizationUserCommand; } /// @@ -120,16 +118,11 @@ public class MembersController : Controller var organizationUserUserDetails = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(_currentContext.OrganizationId.Value); // TODO: Get all CollectionUser associations for the organization and marry them up here for the response. - if (_featureService.IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization)) + var orgUsersTwoFactorIsEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(organizationUserUserDetails); + var memberResponses = organizationUserUserDetails.Select(u => { - return await List_vNext(organizationUserUserDetails); - } - - var memberResponsesTasks = organizationUserUserDetails.Select(async u => - { - return new MemberResponseModel(u, await _userService.TwoFactorIsEnabledAsync(u), null); + return new MemberResponseModel(u, orgUsersTwoFactorIsEnabled.FirstOrDefault(tuple => tuple.user == u).twoFactorIsEnabled, null); }); - var memberResponses = await Task.WhenAll(memberResponsesTasks); var response = new ListResponseModel(memberResponses); return new JsonResult(response); } @@ -243,7 +236,7 @@ public class MembersController : Controller { return new NotFoundResult(); } - await _organizationService.RemoveUserAsync(_currentContext.OrganizationId.Value, id, null); + await _removeOrganizationUserCommand.RemoveUserAsync(_currentContext.OrganizationId.Value, id, null); return new OkResult(); } @@ -268,15 +261,4 @@ public class MembersController : Controller await _organizationService.ResendInviteAsync(_currentContext.OrganizationId.Value, null, id); return new OkResult(); } - - private async Task List_vNext(ICollection organizationUserUserDetails) - { - var orgUsersTwoFactorIsEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(organizationUserUserDetails); - var memberResponses = organizationUserUserDetails.Select(u => - { - return new MemberResponseModel(u, orgUsersTwoFactorIsEnabled.FirstOrDefault(tuple => tuple.user == u).twoFactorIsEnabled, null); - }); - var response = new ListResponseModel(memberResponses); - return new JsonResult(response); - } } diff --git a/src/Api/AdminConsole/Public/Controllers/PoliciesController.cs b/src/Api/AdminConsole/Public/Controllers/PoliciesController.cs index 6af83e57d..2d83bd705 100644 --- a/src/Api/AdminConsole/Public/Controllers/PoliciesController.cs +++ b/src/Api/AdminConsole/Public/Controllers/PoliciesController.cs @@ -18,20 +18,17 @@ public class PoliciesController : Controller { private readonly IPolicyRepository _policyRepository; private readonly IPolicyService _policyService; - private readonly IUserService _userService; private readonly IOrganizationService _organizationService; private readonly ICurrentContext _currentContext; public PoliciesController( IPolicyRepository policyRepository, IPolicyService policyService, - IUserService userService, IOrganizationService organizationService, ICurrentContext currentContext) { _policyRepository = policyRepository; _policyService = policyService; - _userService = userService; _organizationService = organizationService; _currentContext = currentContext; } @@ -99,7 +96,7 @@ public class PoliciesController : Controller { policy = model.ToPolicy(policy); } - await _policyService.SaveAsync(policy, _userService, _organizationService, null); + await _policyService.SaveAsync(policy, _organizationService, null); var response = new PolicyResponseModel(policy); return new JsonResult(response); } diff --git a/src/Api/Api.csproj b/src/Api/Api.csproj index 1d64c148a..13066ed01 100644 --- a/src/Api/Api.csproj +++ b/src/Api/Api.csproj @@ -35,7 +35,7 @@ - + diff --git a/src/Api/Auth/Controllers/TwoFactorController.cs b/src/Api/Auth/Controllers/TwoFactorController.cs index 0a50f9bc2..f2578fbc6 100644 --- a/src/Api/Auth/Controllers/TwoFactorController.cs +++ b/src/Api/Auth/Controllers/TwoFactorController.cs @@ -3,7 +3,6 @@ using Bit.Api.Auth.Models.Request.Accounts; using Bit.Api.Auth.Models.Response.TwoFactor; using Bit.Api.Models.Request; using Bit.Api.Models.Response; -using Bit.Core; using Bit.Core.Auth.Enums; using Bit.Core.Auth.LoginFeatures.PasswordlessLogin.Interfaces; using Bit.Core.Auth.Models.Business.Tokenables; @@ -37,7 +36,6 @@ public class TwoFactorController : Controller private readonly IFeatureService _featureService; private readonly IDataProtectorTokenFactory _twoFactorAuthenticatorDataProtector; private readonly IDataProtectorTokenFactory _ssoEmailTwoFactorSessionDataProtector; - private readonly bool _TwoFactorAuthenticatorTokenFeatureFlagEnabled; public TwoFactorController( IUserService userService, @@ -61,7 +59,6 @@ public class TwoFactorController : Controller _featureService = featureService; _twoFactorAuthenticatorDataProtector = twoFactorAuthenticatorDataProtector; _ssoEmailTwoFactorSessionDataProtector = ssoEmailTwoFactorSessionDataProtector; - _TwoFactorAuthenticatorTokenFeatureFlagEnabled = _featureService.IsEnabled(FeatureFlagKeys.AuthenticatorTwoFactorToken); } [HttpGet("")] @@ -102,13 +99,10 @@ public class TwoFactorController : Controller public async Task GetAuthenticator( [FromBody] SecretVerificationRequestModel model) { - var user = _TwoFactorAuthenticatorTokenFeatureFlagEnabled ? await CheckAsync(model, false) : await CheckAsync(model, false, true); + var user = await CheckAsync(model, false); var response = new TwoFactorAuthenticatorResponseModel(user); - if (_TwoFactorAuthenticatorTokenFeatureFlagEnabled) - { - var tokenable = new TwoFactorAuthenticatorUserVerificationTokenable(user, response.Key); - response.UserVerificationToken = _twoFactorAuthenticatorDataProtector.Protect(tokenable); - } + var tokenable = new TwoFactorAuthenticatorUserVerificationTokenable(user, response.Key); + response.UserVerificationToken = _twoFactorAuthenticatorDataProtector.Protect(tokenable); return response; } @@ -117,20 +111,11 @@ public class TwoFactorController : Controller public async Task PutAuthenticator( [FromBody] UpdateTwoFactorAuthenticatorRequestModel model) { - User user; - if (_TwoFactorAuthenticatorTokenFeatureFlagEnabled) + var user = model.ToUser(await _userService.GetUserByPrincipalAsync(User)); + _twoFactorAuthenticatorDataProtector.TryUnprotect(model.UserVerificationToken, out var decryptedToken); + if (!decryptedToken.TokenIsValid(user, model.Key)) { - user = model.ToUser(await _userService.GetUserByPrincipalAsync(User)); - _twoFactorAuthenticatorDataProtector.TryUnprotect(model.UserVerificationToken, out var decryptedToken); - if (!decryptedToken.TokenIsValid(user, model.Key)) - { - throw new BadRequestException("UserVerificationToken", "User verification failed."); - } - } - else - { - user = await CheckAsync(model, false); - model.ToUser(user); // populates user obj with proper metadata for VerifyTwoFactorTokenAsync + throw new BadRequestException("UserVerificationToken", "User verification failed."); } if (!await _userManager.VerifyTwoFactorTokenAsync(user, @@ -145,7 +130,6 @@ public class TwoFactorController : Controller return response; } - [RequireFeature(FeatureFlagKeys.AuthenticatorTwoFactorToken)] [HttpDelete("authenticator")] public async Task DisableAuthenticator( [FromBody] TwoFactorAuthenticatorDisableRequestModel model) @@ -157,7 +141,7 @@ public class TwoFactorController : Controller throw new BadRequestException("UserVerificationToken", "User verification failed."); } - await _userService.DisableTwoFactorProviderAsync(user, model.Type.Value, _organizationService); + await _userService.DisableTwoFactorProviderAsync(user, model.Type.Value); return new TwoFactorProviderResponseModel(model.Type.Value, user); } @@ -412,7 +396,7 @@ public class TwoFactorController : Controller public async Task PutDisable([FromBody] TwoFactorProviderRequestModel model) { var user = await CheckAsync(model, false); - await _userService.DisableTwoFactorProviderAsync(user, model.Type.Value, _organizationService); + await _userService.DisableTwoFactorProviderAsync(user, model.Type.Value); var response = new TwoFactorProviderResponseModel(model.Type.Value, user); return response; } @@ -453,8 +437,7 @@ public class TwoFactorController : Controller [AllowAnonymous] public async Task PostRecover([FromBody] TwoFactorRecoveryRequestModel model) { - if (!await _userService.RecoverTwoFactorAsync(model.Email, model.MasterPasswordHash, model.RecoveryCode, - _organizationService)) + if (!await _userService.RecoverTwoFactorAsync(model.Email, model.MasterPasswordHash, model.RecoveryCode)) { await Task.Delay(2000); throw new BadRequestException(string.Empty, "Invalid information. Try again."); diff --git a/src/Api/Billing/Controllers/AccountsBillingController.cs b/src/Api/Billing/Controllers/AccountsBillingController.cs index a72d79673..574ac3e65 100644 --- a/src/Api/Billing/Controllers/AccountsBillingController.cs +++ b/src/Api/Billing/Controllers/AccountsBillingController.cs @@ -1,4 +1,5 @@ -using Bit.Api.Billing.Models.Responses; +#nullable enable +using Bit.Api.Billing.Models.Responses; using Bit.Core.Billing.Services; using Bit.Core.Services; using Bit.Core.Utilities; @@ -43,7 +44,7 @@ public class AccountsBillingController( } [HttpGet("invoices")] - public async Task GetInvoicesAsync([FromQuery] string startAfter = null) + public async Task GetInvoicesAsync([FromQuery] string? status = null, [FromQuery] string? startAfter = null) { var user = await userService.GetUserByPrincipalAsync(User); if (user == null) @@ -54,6 +55,7 @@ public class AccountsBillingController( var invoices = await paymentHistoryService.GetInvoiceHistoryAsync( user, 5, + status, startAfter); return TypedResults.Ok(invoices); diff --git a/src/Api/Billing/Controllers/OrganizationBillingController.cs b/src/Api/Billing/Controllers/OrganizationBillingController.cs index 8d926ec9f..f6ba87c71 100644 --- a/src/Api/Billing/Controllers/OrganizationBillingController.cs +++ b/src/Api/Billing/Controllers/OrganizationBillingController.cs @@ -1,4 +1,5 @@ -using Bit.Api.Billing.Models.Requests; +#nullable enable +using Bit.Api.Billing.Models.Requests; using Bit.Api.Billing.Models.Responses; using Bit.Core; using Bit.Core.Billing.Services; @@ -63,7 +64,7 @@ public class OrganizationBillingController( } [HttpGet("invoices")] - public async Task GetInvoicesAsync([FromRoute] Guid organizationId, [FromQuery] string startAfter = null) + public async Task GetInvoicesAsync([FromRoute] Guid organizationId, [FromQuery] string? status = null, [FromQuery] string? startAfter = null) { if (!await currentContext.ViewBillingHistory(organizationId)) { @@ -80,6 +81,7 @@ public class OrganizationBillingController( var invoices = await paymentHistoryService.GetInvoiceHistoryAsync( organization, 5, + status, startAfter); return TypedResults.Ok(invoices); diff --git a/src/Billing/Startup.cs b/src/Billing/Startup.cs index acb0f120e..7965cbe50 100644 --- a/src/Billing/Startup.cs +++ b/src/Billing/Startup.cs @@ -75,6 +75,7 @@ public class Startup // Services services.AddBaseServices(globalSettings); services.AddDefaultServices(globalSettings); + services.AddDistributedCache(globalSettings); services.AddBillingOperations(); services.TryAddSingleton(); diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteManagedOrganizationUserAccountCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteManagedOrganizationUserAccountCommand.cs index d70d061c8..0bcd16cee 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteManagedOrganizationUserAccountCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteManagedOrganizationUserAccountCommand.cs @@ -18,7 +18,8 @@ public class DeleteManagedOrganizationUserAccountCommand : IDeleteManagedOrganiz private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IUserRepository _userRepository; private readonly ICurrentContext _currentContext; - private readonly IOrganizationService _organizationService; + private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery; + public DeleteManagedOrganizationUserAccountCommand( IUserService userService, IEventService eventService, @@ -26,7 +27,7 @@ public class DeleteManagedOrganizationUserAccountCommand : IDeleteManagedOrganiz IOrganizationUserRepository organizationUserRepository, IUserRepository userRepository, ICurrentContext currentContext, - IOrganizationService organizationService) + IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery) { _userService = userService; _eventService = eventService; @@ -34,7 +35,7 @@ public class DeleteManagedOrganizationUserAccountCommand : IDeleteManagedOrganiz _organizationUserRepository = organizationUserRepository; _userRepository = userRepository; _currentContext = currentContext; - _organizationService = organizationService; + _hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery; } public async Task DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId) @@ -46,7 +47,7 @@ public class DeleteManagedOrganizationUserAccountCommand : IDeleteManagedOrganiz } var managementStatus = await _getOrganizationUsersManagementStatusQuery.GetUsersOrganizationManagementStatusAsync(organizationId, new[] { organizationUserId }); - var hasOtherConfirmedOwners = await _organizationService.HasConfirmedOwnersExceptAsync(organizationId, new[] { organizationUserId }, includeProvider: true); + var hasOtherConfirmedOwners = await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(organizationId, new[] { organizationUserId }, includeProvider: true); await ValidateDeleteUserAsync(organizationId, organizationUser, deletingUserId, managementStatus, hasOtherConfirmedOwners); @@ -67,7 +68,7 @@ public class DeleteManagedOrganizationUserAccountCommand : IDeleteManagedOrganiz var users = await _userRepository.GetManyAsync(userIds); var managementStatus = await _getOrganizationUsersManagementStatusQuery.GetUsersOrganizationManagementStatusAsync(organizationId, orgUserIds); - var hasOtherConfirmedOwners = await _organizationService.HasConfirmedOwnersExceptAsync(organizationId, orgUserIds, includeProvider: true); + var hasOtherConfirmedOwners = await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(organizationId, orgUserIds, includeProvider: true); var results = new List<(Guid OrganizationUserId, string? ErrorMessage)>(); foreach (var orgUserId in orgUserIds) diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/HasConfirmedOwnersExceptQuery.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/HasConfirmedOwnersExceptQuery.cs new file mode 100644 index 000000000..746042ea3 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/HasConfirmedOwnersExceptQuery.cs @@ -0,0 +1,41 @@ +using Bit.Core.AdminConsole.Enums.Provider; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Repositories; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers; + +public class HasConfirmedOwnersExceptQuery : IHasConfirmedOwnersExceptQuery +{ + private readonly IProviderUserRepository _providerUserRepository; + private readonly IOrganizationUserRepository _organizationUserRepository; + + public HasConfirmedOwnersExceptQuery( + IProviderUserRepository providerUserRepository, + IOrganizationUserRepository organizationUserRepository) + { + _providerUserRepository = providerUserRepository; + _organizationUserRepository = organizationUserRepository; + } + + public async Task HasConfirmedOwnersExceptAsync(Guid organizationId, IEnumerable organizationUsersId, bool includeProvider = true) + { + var confirmedOwners = await GetConfirmedOwnersAsync(organizationId); + var confirmedOwnersIds = confirmedOwners.Select(u => u.Id); + bool hasOtherOwner = confirmedOwnersIds.Except(organizationUsersId).Any(); + if (!hasOtherOwner && includeProvider) + { + return (await _providerUserRepository.GetManyByOrganizationAsync(organizationId, ProviderUserStatusType.Confirmed)).Any(); + } + return hasOtherOwner; + } + + private async Task> GetConfirmedOwnersAsync(Guid organizationId) + { + var owners = await _organizationUserRepository.GetManyByOrganizationAsync(organizationId, + OrganizationUserType.Owner); + return owners.Where(o => o.Status == OrganizationUserStatusType.Confirmed); + } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IHasConfirmedOwnersExceptQuery.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IHasConfirmedOwnersExceptQuery.cs new file mode 100644 index 000000000..2964f7116 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IHasConfirmedOwnersExceptQuery.cs @@ -0,0 +1,12 @@ +namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; + +public interface IHasConfirmedOwnersExceptQuery +{ + /// + /// Checks if an organization has any confirmed owners except for the ones in the list. + /// + /// The organization ID. + /// The organization user IDs to exclude. + /// Whether to include the provider users in the count. + Task HasConfirmedOwnersExceptAsync(Guid organizationId, IEnumerable organizationUsersId, bool includeProvider = true); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IRemoveOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IRemoveOrganizationUserCommand.cs index 3213762ea..583645a89 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IRemoveOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IRemoveOrganizationUserCommand.cs @@ -1,4 +1,5 @@ -using Bit.Core.Enums; +using Bit.Core.Entities; +using Bit.Core.Enums; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; @@ -7,4 +8,7 @@ public interface IRemoveOrganizationUserCommand Task RemoveUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId); Task RemoveUserAsync(Guid organizationId, Guid organizationUserId, EventSystemUser eventSystemUser); + Task RemoveUserAsync(Guid organizationId, Guid userId); + Task>> RemoveUsersAsync(Guid organizationId, + IEnumerable organizationUserIds, Guid? deletingUserId); } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommand.cs index e2aea0249..09444306e 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommand.cs @@ -1,4 +1,6 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core.Context; +using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Repositories; @@ -8,38 +10,171 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers; public class RemoveOrganizationUserCommand : IRemoveOrganizationUserCommand { + private readonly IDeviceRepository _deviceRepository; private readonly IOrganizationUserRepository _organizationUserRepository; - private readonly IOrganizationService _organizationService; + private readonly IEventService _eventService; + private readonly IPushNotificationService _pushNotificationService; + private readonly IPushRegistrationService _pushRegistrationService; + private readonly ICurrentContext _currentContext; + private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery; public RemoveOrganizationUserCommand( + IDeviceRepository deviceRepository, IOrganizationUserRepository organizationUserRepository, - IOrganizationService organizationService - ) + IEventService eventService, + IPushNotificationService pushNotificationService, + IPushRegistrationService pushRegistrationService, + ICurrentContext currentContext, + IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery) { + _deviceRepository = deviceRepository; _organizationUserRepository = organizationUserRepository; - _organizationService = organizationService; + _eventService = eventService; + _pushNotificationService = pushNotificationService; + _pushRegistrationService = pushRegistrationService; + _currentContext = currentContext; + _hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery; } public async Task RemoveUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId) { - await ValidateDeleteUserAsync(organizationId, organizationUserId); + var organizationUser = await _organizationUserRepository.GetByIdAsync(organizationUserId); + ValidateDeleteUser(organizationId, organizationUser); - await _organizationService.RemoveUserAsync(organizationId, organizationUserId, deletingUserId); + await RepositoryDeleteUserAsync(organizationUser, deletingUserId); + + await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed); } public async Task RemoveUserAsync(Guid organizationId, Guid organizationUserId, EventSystemUser eventSystemUser) { - await ValidateDeleteUserAsync(organizationId, organizationUserId); + var organizationUser = await _organizationUserRepository.GetByIdAsync(organizationUserId); + ValidateDeleteUser(organizationId, organizationUser); - await _organizationService.RemoveUserAsync(organizationId, organizationUserId, eventSystemUser); + await RepositoryDeleteUserAsync(organizationUser, null); + + await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed, eventSystemUser); } - private async Task ValidateDeleteUserAsync(Guid organizationId, Guid organizationUserId) + public async Task RemoveUserAsync(Guid organizationId, Guid userId) + { + var organizationUser = await _organizationUserRepository.GetByOrganizationAsync(organizationId, userId); + ValidateDeleteUser(organizationId, organizationUser); + + await RepositoryDeleteUserAsync(organizationUser, null); + + await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed); + } + + public async Task>> RemoveUsersAsync(Guid organizationId, + IEnumerable organizationUsersId, + Guid? deletingUserId) + { + var orgUsers = await _organizationUserRepository.GetManyAsync(organizationUsersId); + var filteredUsers = orgUsers.Where(u => u.OrganizationId == organizationId) + .ToList(); + + if (!filteredUsers.Any()) + { + throw new BadRequestException("Users invalid."); + } + + if (!await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(organizationId, organizationUsersId)) + { + throw new BadRequestException("Organization must have at least one confirmed owner."); + } + + var deletingUserIsOwner = false; + if (deletingUserId.HasValue) + { + deletingUserIsOwner = await _currentContext.OrganizationOwner(organizationId); + } + + var result = new List>(); + var deletedUserIds = new List(); + foreach (var orgUser in filteredUsers) + { + try + { + if (deletingUserId.HasValue && orgUser.UserId == deletingUserId) + { + throw new BadRequestException("You cannot remove yourself."); + } + + if (orgUser.Type == OrganizationUserType.Owner && deletingUserId.HasValue && !deletingUserIsOwner) + { + throw new BadRequestException("Only owners can delete other owners."); + } + + await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Removed); + + if (orgUser.UserId.HasValue) + { + await DeleteAndPushUserRegistrationAsync(organizationId, orgUser.UserId.Value); + } + result.Add(Tuple.Create(orgUser, "")); + deletedUserIds.Add(orgUser.Id); + } + catch (BadRequestException e) + { + result.Add(Tuple.Create(orgUser, e.Message)); + } + + await _organizationUserRepository.DeleteManyAsync(deletedUserIds); + } + + return result; + } + + private void ValidateDeleteUser(Guid organizationId, OrganizationUser orgUser) { - var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId); if (orgUser == null || orgUser.OrganizationId != organizationId) { throw new NotFoundException("User not found."); } } + + private async Task RepositoryDeleteUserAsync(OrganizationUser orgUser, Guid? deletingUserId) + { + if (deletingUserId.HasValue && orgUser.UserId == deletingUserId.Value) + { + throw new BadRequestException("You cannot remove yourself."); + } + + if (orgUser.Type == OrganizationUserType.Owner) + { + if (deletingUserId.HasValue && !await _currentContext.OrganizationOwner(orgUser.OrganizationId)) + { + throw new BadRequestException("Only owners can delete other owners."); + } + + if (!await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(orgUser.OrganizationId, new[] { orgUser.Id }, includeProvider: true)) + { + throw new BadRequestException("Organization must have at least one confirmed owner."); + } + } + + await _organizationUserRepository.DeleteAsync(orgUser); + + if (orgUser.UserId.HasValue) + { + await DeleteAndPushUserRegistrationAsync(orgUser.OrganizationId, orgUser.UserId.Value); + } + } + + private async Task>> GetUserDeviceIdsAsync(Guid userId) + { + var devices = await _deviceRepository.GetManyByUserIdAsync(userId); + return devices + .Where(d => !string.IsNullOrWhiteSpace(d.PushToken)) + .Select(d => new KeyValuePair(d.Id.ToString(), d.Type)); + } + + private async Task DeleteAndPushUserRegistrationAsync(Guid organizationId, Guid userId) + { + var devices = await GetUserDeviceIdsAsync(userId); + await _pushRegistrationService.DeleteUserRegistrationOrganizationAsync(devices, + organizationId.ToString()); + await _pushNotificationService.PushSyncOrgKeysAsync(userId); + } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommand.cs index cf8068c5b..c5a4b3da1 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommand.cs @@ -22,6 +22,7 @@ public class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand; private readonly ICollectionRepository _collectionRepository; private readonly IGroupRepository _groupRepository; + private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery; public UpdateOrganizationUserCommand( IEventService eventService, @@ -31,7 +32,8 @@ public class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand ICountNewSmSeatsRequiredQuery countNewSmSeatsRequiredQuery, IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand, ICollectionRepository collectionRepository, - IGroupRepository groupRepository) + IGroupRepository groupRepository, + IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery) { _eventService = eventService; _organizationService = organizationService; @@ -41,6 +43,7 @@ public class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand _updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand; _collectionRepository = collectionRepository; _groupRepository = groupRepository; + _hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery; } /// @@ -87,7 +90,7 @@ public class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand await _organizationService.ValidateOrganizationCustomPermissionsEnabledAsync(user.OrganizationId, user.Type); if (user.Type != OrganizationUserType.Owner && - !await _organizationService.HasConfirmedOwnersExceptAsync(user.OrganizationId, new[] { user.Id })) + !await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(user.OrganizationId, new[] { user.Id })) { throw new BadRequestException("Organization must have at least one confirmed owner."); } diff --git a/src/Core/AdminConsole/Services/IOrganizationService.cs b/src/Core/AdminConsole/Services/IOrganizationService.cs index aaa2f86c8..646ae6616 100644 --- a/src/Core/AdminConsole/Services/IOrganizationService.cs +++ b/src/Core/AdminConsole/Services/IOrganizationService.cs @@ -49,32 +49,21 @@ public interface IOrganizationService IEnumerable<(OrganizationUserInvite invite, string externalId)> invites); Task>> ResendInvitesAsync(Guid organizationId, Guid? invitingUserId, IEnumerable organizationUsersId); Task ResendInviteAsync(Guid organizationId, Guid? invitingUserId, Guid organizationUserId, bool initOrganization = false); - Task ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key, - Guid confirmingUserId, IUserService userService); + Task ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key, Guid confirmingUserId); Task>> ConfirmUsersAsync(Guid organizationId, Dictionary keys, - Guid confirmingUserId, IUserService userService); - Task>> ConfirmUsersAsync_vNext(Guid organizationId, Dictionary keys, Guid confirmingUserId); - [Obsolete("IRemoveOrganizationUserCommand should be used instead. To be removed by EC-607.")] - Task RemoveUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId); - [Obsolete("IRemoveOrganizationUserCommand should be used instead. To be removed by EC-607.")] - Task RemoveUserAsync(Guid organizationId, Guid organizationUserId, EventSystemUser systemUser); - Task RemoveUserAsync(Guid organizationId, Guid userId); - Task>> RemoveUsersAsync(Guid organizationId, - IEnumerable organizationUserIds, Guid? deletingUserId); Task UpdateUserResetPasswordEnrollmentAsync(Guid organizationId, Guid userId, string resetPasswordKey, Guid? callingUserId); Task ImportAsync(Guid organizationId, IEnumerable groups, IEnumerable newUsers, IEnumerable removeUserExternalIds, bool overwriteExisting, EventSystemUser eventSystemUser); Task DeleteSsoUserAsync(Guid userId, Guid? organizationId); Task UpdateOrganizationKeysAsync(Guid orgId, string publicKey, string privateKey); - Task HasConfirmedOwnersExceptAsync(Guid organizationId, IEnumerable organizationUsersId, bool includeProvider = true); Task RevokeUserAsync(OrganizationUser organizationUser, Guid? revokingUserId); Task RevokeUserAsync(OrganizationUser organizationUser, EventSystemUser systemUser); Task>> RevokeUsersAsync(Guid organizationId, IEnumerable organizationUserIds, Guid? revokingUserId); - Task RestoreUserAsync(OrganizationUser organizationUser, Guid? restoringUserId, IUserService userService); - Task RestoreUserAsync(OrganizationUser organizationUser, EventSystemUser systemUser, IUserService userService); + Task RestoreUserAsync(OrganizationUser organizationUser, Guid? restoringUserId); + Task RestoreUserAsync(OrganizationUser organizationUser, EventSystemUser systemUser); Task>> RestoreUsersAsync(Guid organizationId, IEnumerable organizationUserIds, Guid? restoringUserId, IUserService userService); Task CreatePendingOrganization(Organization organization, string ownerEmail, ClaimsPrincipal user, IUserService userService, bool salesAssistedTrialStarted); diff --git a/src/Core/AdminConsole/Services/IPolicyService.cs b/src/Core/AdminConsole/Services/IPolicyService.cs index e2f2fa794..6d92a3a4f 100644 --- a/src/Core/AdminConsole/Services/IPolicyService.cs +++ b/src/Core/AdminConsole/Services/IPolicyService.cs @@ -10,8 +10,7 @@ namespace Bit.Core.AdminConsole.Services; public interface IPolicyService { - Task SaveAsync(Policy policy, IUserService userService, IOrganizationService organizationService, - Guid? savingUserId); + Task SaveAsync(Policy policy, IOrganizationService organizationService, Guid? savingUserId); /// /// Get the combined master password policy options for the specified user. diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs index 5c3f81cee..f453a22b4 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs @@ -73,6 +73,7 @@ public class OrganizationService : IOrganizationService private readonly IFeatureService _featureService; private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; private readonly IOrganizationBillingService _organizationBillingService; + private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery; public OrganizationService( IOrganizationRepository organizationRepository, @@ -107,7 +108,8 @@ public class OrganizationService : IOrganizationService IProviderRepository providerRepository, IFeatureService featureService, ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, - IOrganizationBillingService organizationBillingService) + IOrganizationBillingService organizationBillingService, + IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery) { _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; @@ -142,6 +144,7 @@ public class OrganizationService : IOrganizationService _featureService = featureService; _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; _organizationBillingService = organizationBillingService; + _hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery; } public async Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken, @@ -1074,7 +1077,7 @@ public class OrganizationService : IOrganizationService } var invitedAreAllOwners = invites.All(i => i.invite.Type == OrganizationUserType.Owner); - if (!invitedAreAllOwners && !await HasConfirmedOwnersExceptAsync(organizationId, new Guid[] { }, includeProvider: true)) + if (!invitedAreAllOwners && !await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(organizationId, new Guid[] { }, includeProvider: true)) { throw new BadRequestException("Organization must have at least one confirmed owner."); } @@ -1323,13 +1326,12 @@ public class OrganizationService : IOrganizationService } public async Task ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key, - Guid confirmingUserId, IUserService userService) + Guid confirmingUserId) { - var result = _featureService.IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization) - ? await ConfirmUsersAsync_vNext(organizationId, new Dictionary() { { organizationUserId, key } }, - confirmingUserId) - : await ConfirmUsersAsync(organizationId, new Dictionary() { { organizationUserId, key } }, - confirmingUserId, userService); + var result = await ConfirmUsersAsync( + organizationId, + new Dictionary() { { organizationUserId, key } }, + confirmingUserId); if (!result.Any()) { @@ -1345,75 +1347,6 @@ public class OrganizationService : IOrganizationService } public async Task>> ConfirmUsersAsync(Guid organizationId, Dictionary keys, - Guid confirmingUserId, IUserService userService) - { - var organizationUsers = await _organizationUserRepository.GetManyAsync(keys.Keys); - var validOrganizationUsers = organizationUsers - .Where(u => u.Status == OrganizationUserStatusType.Accepted && u.OrganizationId == organizationId && u.UserId != null) - .ToList(); - - if (!validOrganizationUsers.Any()) - { - return new List>(); - } - - var validOrganizationUserIds = validOrganizationUsers.Select(u => u.UserId.Value).ToList(); - - var organization = await GetOrgById(organizationId); - var usersOrgs = await _organizationUserRepository.GetManyByManyUsersAsync(validOrganizationUserIds); - var users = await _userRepository.GetManyAsync(validOrganizationUserIds); - - var keyedFilteredUsers = validOrganizationUsers.ToDictionary(u => u.UserId.Value, u => u); - var keyedOrganizationUsers = usersOrgs.GroupBy(u => u.UserId.Value) - .ToDictionary(u => u.Key, u => u.ToList()); - - var succeededUsers = new List(); - var result = new List>(); - - foreach (var user in users) - { - if (!keyedFilteredUsers.ContainsKey(user.Id)) - { - continue; - } - var orgUser = keyedFilteredUsers[user.Id]; - var orgUsers = keyedOrganizationUsers.GetValueOrDefault(user.Id, new List()); - try - { - if (organization.PlanType == PlanType.Free && (orgUser.Type == OrganizationUserType.Admin - || orgUser.Type == OrganizationUserType.Owner)) - { - // Since free organizations only supports a few users there is not much point in avoiding N+1 queries for this. - var adminCount = await _organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(user.Id); - if (adminCount > 0) - { - throw new BadRequestException("User can only be an admin of one free organization."); - } - } - - await CheckPolicies(organizationId, user, orgUsers, userService); - orgUser.Status = OrganizationUserStatusType.Confirmed; - orgUser.Key = keys[orgUser.Id]; - orgUser.Email = null; - - await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed); - await _mailService.SendOrganizationConfirmedEmailAsync(organization.DisplayName(), user.Email, orgUser.AccessSecretsManager); - await DeleteAndPushUserRegistrationAsync(organizationId, user.Id); - succeededUsers.Add(orgUser); - result.Add(Tuple.Create(orgUser, "")); - } - catch (BadRequestException e) - { - result.Add(Tuple.Create(orgUser, e.Message)); - } - } - - await _organizationUserRepository.ReplaceManyAsync(succeededUsers); - - return result; - } - - public async Task>> ConfirmUsersAsync_vNext(Guid organizationId, Dictionary keys, Guid confirmingUserId) { var selectedOrganizationUsers = await _organizationUserRepository.GetManyAsync(keys.Keys); @@ -1430,7 +1363,7 @@ public class OrganizationService : IOrganizationService var organization = await GetOrgById(organizationId); var allUsersOrgs = await _organizationUserRepository.GetManyByManyUsersAsync(validSelectedUserIds); - var users = await _userRepository.GetManyWithCalculatedPremiumAsync(validSelectedUserIds); + var users = await _userRepository.GetManyAsync(validSelectedUserIds); var usersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(validSelectedUserIds); var keyedFilteredUsers = validSelectedOrganizationUsers.ToDictionary(u => u.UserId.Value, u => u); @@ -1462,7 +1395,7 @@ public class OrganizationService : IOrganizationService } var twoFactorEnabled = usersTwoFactorEnabled.FirstOrDefault(tuple => tuple.userId == user.Id).twoFactorIsEnabled; - await CheckPolicies_vNext(organizationId, user, orgUsers, twoFactorEnabled); + await CheckPoliciesAsync(organizationId, user, orgUsers, twoFactorEnabled); orgUser.Status = OrganizationUserStatusType.Confirmed; orgUser.Key = keys[orgUser.Id]; orgUser.Email = null; @@ -1567,33 +1500,7 @@ public class OrganizationService : IOrganizationService } } - private async Task CheckPolicies(Guid organizationId, User user, - ICollection userOrgs, IUserService userService) - { - // Enforce Two Factor Authentication Policy for this organization - var orgRequiresTwoFactor = (await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication)).Any(p => p.OrganizationId == organizationId); - if (orgRequiresTwoFactor && !await userService.TwoFactorIsEnabledAsync(user)) - { - throw new BadRequestException("User does not have two-step login enabled."); - } - - var hasOtherOrgs = userOrgs.Any(ou => ou.OrganizationId != organizationId); - var singleOrgPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg); - var otherSingleOrgPolicies = - singleOrgPolicies.Where(p => p.OrganizationId != organizationId); - // Enforce Single Organization Policy for this organization - if (hasOtherOrgs && singleOrgPolicies.Any(p => p.OrganizationId == organizationId)) - { - throw new BadRequestException("Cannot confirm this member to the organization until they leave or remove all other organizations."); - } - // Enforce Single Organization Policy of other organizations user is a member of - if (otherSingleOrgPolicies.Any()) - { - throw new BadRequestException("Cannot confirm this member to the organization because they are in another organization which forbids it."); - } - } - - private async Task CheckPolicies_vNext(Guid organizationId, UserWithCalculatedPremium user, + private async Task CheckPoliciesAsync(Guid organizationId, User user, ICollection userOrgs, bool twoFactorEnabled) { // Enforce Two Factor Authentication Policy for this organization @@ -1620,149 +1527,6 @@ public class OrganizationService : IOrganizationService } } - [Obsolete("IRemoveOrganizationUserCommand should be used instead. To be removed by EC-607.")] - public async Task RemoveUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId) - { - var orgUser = await RepositoryDeleteUserAsync(organizationId, organizationUserId, deletingUserId); - await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Removed); - } - - [Obsolete("IRemoveOrganizationUserCommand should be used instead. To be removed by EC-607.")] - public async Task RemoveUserAsync(Guid organizationId, Guid organizationUserId, - EventSystemUser systemUser) - { - var orgUser = await RepositoryDeleteUserAsync(organizationId, organizationUserId, null); - await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Removed, systemUser); - } - - private async Task RepositoryDeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId) - { - var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId); - if (orgUser == null || orgUser.OrganizationId != organizationId) - { - throw new BadRequestException("User not valid."); - } - - if (deletingUserId.HasValue && orgUser.UserId == deletingUserId.Value) - { - throw new BadRequestException("You cannot remove yourself."); - } - - if (orgUser.Type == OrganizationUserType.Owner && deletingUserId.HasValue && - !await _currentContext.OrganizationOwner(organizationId)) - { - throw new BadRequestException("Only owners can delete other owners."); - } - - if (!await HasConfirmedOwnersExceptAsync(organizationId, new[] { organizationUserId }, includeProvider: true)) - { - throw new BadRequestException("Organization must have at least one confirmed owner."); - } - - await _organizationUserRepository.DeleteAsync(orgUser); - - if (orgUser.UserId.HasValue) - { - await DeleteAndPushUserRegistrationAsync(organizationId, orgUser.UserId.Value); - } - - return orgUser; - } - - public async Task RemoveUserAsync(Guid organizationId, Guid userId) - { - var orgUser = await _organizationUserRepository.GetByOrganizationAsync(organizationId, userId); - if (orgUser == null) - { - throw new NotFoundException(); - } - - if (!await HasConfirmedOwnersExceptAsync(organizationId, new[] { orgUser.Id })) - { - throw new BadRequestException("Organization must have at least one confirmed owner."); - } - - await _organizationUserRepository.DeleteAsync(orgUser); - await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Removed); - - if (orgUser.UserId.HasValue) - { - await DeleteAndPushUserRegistrationAsync(organizationId, orgUser.UserId.Value); - } - } - - public async Task>> RemoveUsersAsync(Guid organizationId, - IEnumerable organizationUsersId, - Guid? deletingUserId) - { - var orgUsers = await _organizationUserRepository.GetManyAsync(organizationUsersId); - var filteredUsers = orgUsers.Where(u => u.OrganizationId == organizationId) - .ToList(); - - if (!filteredUsers.Any()) - { - throw new BadRequestException("Users invalid."); - } - - if (!await HasConfirmedOwnersExceptAsync(organizationId, organizationUsersId)) - { - throw new BadRequestException("Organization must have at least one confirmed owner."); - } - - var deletingUserIsOwner = false; - if (deletingUserId.HasValue) - { - deletingUserIsOwner = await _currentContext.OrganizationOwner(organizationId); - } - - var result = new List>(); - var deletedUserIds = new List(); - foreach (var orgUser in filteredUsers) - { - try - { - if (deletingUserId.HasValue && orgUser.UserId == deletingUserId) - { - throw new BadRequestException("You cannot remove yourself."); - } - - if (orgUser.Type == OrganizationUserType.Owner && deletingUserId.HasValue && !deletingUserIsOwner) - { - throw new BadRequestException("Only owners can delete other owners."); - } - - await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Removed); - - if (orgUser.UserId.HasValue) - { - await DeleteAndPushUserRegistrationAsync(organizationId, orgUser.UserId.Value); - } - result.Add(Tuple.Create(orgUser, "")); - deletedUserIds.Add(orgUser.Id); - } - catch (BadRequestException e) - { - result.Add(Tuple.Create(orgUser, e.Message)); - } - - await _organizationUserRepository.DeleteManyAsync(deletedUserIds); - } - - return result; - } - - public async Task HasConfirmedOwnersExceptAsync(Guid organizationId, IEnumerable organizationUsersId, bool includeProvider = true) - { - var confirmedOwners = await GetConfirmedOwnersAsync(organizationId); - var confirmedOwnersIds = confirmedOwners.Select(u => u.Id); - bool hasOtherOwner = confirmedOwnersIds.Except(organizationUsersId).Any(); - if (!hasOtherOwner && includeProvider) - { - return (await _providerUserRepository.GetManyByOrganizationAsync(organizationId, ProviderUserStatusType.Confirmed)).Any(); - } - return hasOtherOwner; - } - public async Task UpdateUserResetPasswordEnrollmentAsync(Guid organizationId, Guid userId, string resetPasswordKey, Guid? callingUserId) { // Org User must be the same as the calling user and the organization ID associated with the user must match passed org ID @@ -2059,13 +1823,6 @@ public class OrganizationService : IOrganizationService await _groupRepository.UpdateUsersAsync(group.Id, users); } - private async Task> GetConfirmedOwnersAsync(Guid organizationId) - { - var owners = await _organizationUserRepository.GetManyByOrganizationAsync(organizationId, - OrganizationUserType.Owner); - return owners.Where(o => o.Status == OrganizationUserStatusType.Confirmed); - } - private async Task DeleteAndPushUserRegistrationAsync(Guid organizationId, Guid userId) { var devices = await GetUserDeviceIdsAsync(userId); @@ -2370,7 +2127,7 @@ public class OrganizationService : IOrganizationService throw new BadRequestException("Already revoked."); } - if (!await HasConfirmedOwnersExceptAsync(organizationUser.OrganizationId, new[] { organizationUser.Id }, includeProvider: true)) + if (!await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(organizationUser.OrganizationId, new[] { organizationUser.Id }, includeProvider: true)) { throw new BadRequestException("Organization must have at least one confirmed owner."); } @@ -2391,7 +2148,7 @@ public class OrganizationService : IOrganizationService throw new BadRequestException("Users invalid."); } - if (!await HasConfirmedOwnersExceptAsync(organizationId, organizationUserIds)) + if (!await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(organizationId, organizationUserIds)) { throw new BadRequestException("Organization must have at least one confirmed owner."); } @@ -2438,8 +2195,7 @@ public class OrganizationService : IOrganizationService return result; } - public async Task RestoreUserAsync(OrganizationUser organizationUser, Guid? restoringUserId, - IUserService userService) + public async Task RestoreUserAsync(OrganizationUser organizationUser, Guid? restoringUserId) { if (restoringUserId.HasValue && organizationUser.UserId == restoringUserId.Value) { @@ -2452,18 +2208,17 @@ public class OrganizationService : IOrganizationService throw new BadRequestException("Only owners can restore other owners."); } - await RepositoryRestoreUserAsync(organizationUser, userService); + await RepositoryRestoreUserAsync(organizationUser); await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored); } - public async Task RestoreUserAsync(OrganizationUser organizationUser, EventSystemUser systemUser, - IUserService userService) + public async Task RestoreUserAsync(OrganizationUser organizationUser, EventSystemUser systemUser) { - await RepositoryRestoreUserAsync(organizationUser, userService); + await RepositoryRestoreUserAsync(organizationUser); await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored, systemUser); } - private async Task RepositoryRestoreUserAsync(OrganizationUser organizationUser, IUserService userService) + private async Task RepositoryRestoreUserAsync(OrganizationUser organizationUser) { if (organizationUser.Status != OrganizationUserStatusType.Revoked) { @@ -2478,21 +2233,14 @@ public class OrganizationService : IOrganizationService await AutoAddSeatsAsync(organization, 1); } - if (_featureService.IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization)) + var userTwoFactorIsEnabled = false; + // Only check Two Factor Authentication status if the user is linked to a user account + if (organizationUser.UserId.HasValue) { - var userTwoFactorIsEnabled = false; - // Only check Two Factor Authentication status if the user is linked to a user account - if (organizationUser.UserId.HasValue) - { - userTwoFactorIsEnabled = (await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(new[] { organizationUser.UserId.Value })).FirstOrDefault().twoFactorIsEnabled; - } + userTwoFactorIsEnabled = (await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(new[] { organizationUser.UserId.Value })).FirstOrDefault().twoFactorIsEnabled; + } - await CheckPoliciesBeforeRestoreAsync_vNext(organizationUser, userTwoFactorIsEnabled); - } - else - { - await CheckPoliciesBeforeRestoreAsync(organizationUser, userService); - } + await CheckPoliciesBeforeRestoreAsync(organizationUser, userTwoFactorIsEnabled); var status = GetPriorActiveOrganizationUserStatusType(organizationUser); @@ -2526,11 +2274,7 @@ public class OrganizationService : IOrganizationService // Query Two Factor Authentication status for all users in the organization // This is an optimization to avoid querying the Two Factor Authentication status for each user individually - IEnumerable<(Guid userId, bool twoFactorIsEnabled)> organizationUsersTwoFactorEnabled = null; - if (_featureService.IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization)) - { - organizationUsersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(filteredUsers.Select(ou => ou.UserId.Value)); - } + var organizationUsersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(filteredUsers.Select(ou => ou.UserId.Value)); var result = new List>(); @@ -2553,15 +2297,8 @@ public class OrganizationService : IOrganizationService throw new BadRequestException("Only owners can restore other owners."); } - if (_featureService.IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization)) - { - var twoFactorIsEnabled = organizationUsersTwoFactorEnabled.FirstOrDefault(ou => ou.userId == organizationUser.UserId.Value).twoFactorIsEnabled; - await CheckPoliciesBeforeRestoreAsync_vNext(organizationUser, twoFactorIsEnabled); - } - else - { - await CheckPoliciesBeforeRestoreAsync(organizationUser, userService); - } + var twoFactorIsEnabled = organizationUsersTwoFactorEnabled.FirstOrDefault(ou => ou.userId == organizationUser.UserId.Value).twoFactorIsEnabled; + await CheckPoliciesBeforeRestoreAsync(organizationUser, twoFactorIsEnabled); var status = GetPriorActiveOrganizationUserStatusType(organizationUser); @@ -2580,54 +2317,7 @@ public class OrganizationService : IOrganizationService return result; } - private async Task CheckPoliciesBeforeRestoreAsync(OrganizationUser orgUser, IUserService userService) - { - // An invited OrganizationUser isn't linked with a user account yet, so these checks are irrelevant - // The user will be subject to the same checks when they try to accept the invite - if (GetPriorActiveOrganizationUserStatusType(orgUser) == OrganizationUserStatusType.Invited) - { - return; - } - - var userId = orgUser.UserId.Value; - - // Enforce Single Organization Policy of organization user is being restored to - var allOrgUsers = await _organizationUserRepository.GetManyByUserAsync(userId); - var hasOtherOrgs = allOrgUsers.Any(ou => ou.OrganizationId != orgUser.OrganizationId); - var singleOrgPoliciesApplyingToRevokedUsers = await _policyService.GetPoliciesApplicableToUserAsync(userId, - PolicyType.SingleOrg, OrganizationUserStatusType.Revoked); - var singleOrgPolicyApplies = singleOrgPoliciesApplyingToRevokedUsers.Any(p => p.OrganizationId == orgUser.OrganizationId); - - if (hasOtherOrgs && singleOrgPolicyApplies) - { - throw new BadRequestException("You cannot restore this user until " + - "they leave or remove all other organizations."); - } - - // Enforce Single Organization Policy of other organizations user is a member of - var anySingleOrgPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(userId, - PolicyType.SingleOrg); - if (anySingleOrgPolicies) - { - throw new BadRequestException("You cannot restore this user because they are a member of " + - "another organization which forbids it"); - } - - // Enforce Two Factor Authentication Policy of organization user is trying to join - var user = await _userRepository.GetByIdAsync(userId); - if (!await userService.TwoFactorIsEnabledAsync(user)) - { - var invitedTwoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(userId, - PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Invited); - if (invitedTwoFactorPolicies.Any(p => p.OrganizationId == orgUser.OrganizationId)) - { - throw new BadRequestException("You cannot restore this user until they enable " + - "two-step login on their user account."); - } - } - } - - private async Task CheckPoliciesBeforeRestoreAsync_vNext(OrganizationUser orgUser, bool userHasTwoFactorEnabled) + private async Task CheckPoliciesBeforeRestoreAsync(OrganizationUser orgUser, bool userHasTwoFactorEnabled) { // An invited OrganizationUser isn't linked with a user account yet, so these checks are irrelevant // The user will be subject to the same checks when they try to accept the invite diff --git a/src/Core/AdminConsole/Services/Implementations/PolicyService.cs b/src/Core/AdminConsole/Services/Implementations/PolicyService.cs index 1ffa2a0e2..7e689f034 100644 --- a/src/Core/AdminConsole/Services/Implementations/PolicyService.cs +++ b/src/Core/AdminConsole/Services/Implementations/PolicyService.cs @@ -1,6 +1,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Repositories; @@ -25,8 +26,8 @@ public class PolicyService : IPolicyService private readonly ISsoConfigRepository _ssoConfigRepository; private readonly IMailService _mailService; private readonly GlobalSettings _globalSettings; - private readonly IFeatureService _featureService; private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; + private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; public PolicyService( IApplicationCacheService applicationCacheService, @@ -37,8 +38,8 @@ public class PolicyService : IPolicyService ISsoConfigRepository ssoConfigRepository, IMailService mailService, GlobalSettings globalSettings, - IFeatureService featureService, - ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery) + ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, + IRemoveOrganizationUserCommand removeOrganizationUserCommand) { _applicationCacheService = applicationCacheService; _eventService = eventService; @@ -48,12 +49,11 @@ public class PolicyService : IPolicyService _ssoConfigRepository = ssoConfigRepository; _mailService = mailService; _globalSettings = globalSettings; - _featureService = featureService; _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; + _removeOrganizationUserCommand = removeOrganizationUserCommand; } - public async Task SaveAsync(Policy policy, IUserService userService, IOrganizationService organizationService, - Guid? savingUserId) + public async Task SaveAsync(Policy policy, IOrganizationService organizationService, Guid? savingUserId) { var org = await _organizationRepository.GetByIdAsync(policy.OrganizationId); if (org == null) @@ -88,14 +88,7 @@ public class PolicyService : IPolicyService return; } - if (_featureService.IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization)) - { - await EnablePolicy_vNext(policy, org, organizationService, savingUserId); - return; - } - - await EnablePolicy(policy, org, userService, organizationService, savingUserId); - return; + await EnablePolicyAsync(policy, org, organizationService, savingUserId); } public async Task GetMasterPasswordPolicyForUserAsync(User user) @@ -269,62 +262,7 @@ public class PolicyService : IPolicyService await _eventService.LogPolicyEventAsync(policy, EventType.Policy_Updated); } - private async Task EnablePolicy(Policy policy, Organization org, IUserService userService, IOrganizationService organizationService, Guid? savingUserId) - { - var currentPolicy = await _policyRepository.GetByIdAsync(policy.Id); - if (!currentPolicy?.Enabled ?? true) - { - var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(policy.OrganizationId); - var removableOrgUsers = orgUsers.Where(ou => - ou.Status != OrganizationUserStatusType.Invited && ou.Status != OrganizationUserStatusType.Revoked && - ou.Type != OrganizationUserType.Owner && ou.Type != OrganizationUserType.Admin && - ou.UserId != savingUserId); - switch (policy.Type) - { - case PolicyType.TwoFactorAuthentication: - // Reorder by HasMasterPassword to prioritize checking users without a master if they have 2FA enabled - foreach (var orgUser in removableOrgUsers.OrderBy(ou => ou.HasMasterPassword)) - { - if (!await userService.TwoFactorIsEnabledAsync(orgUser)) - { - if (!orgUser.HasMasterPassword) - { - throw new BadRequestException( - "Policy could not be enabled. Non-compliant members will lose access to their accounts. Identify members without two-step login from the policies column in the members page."); - } - - await organizationService.RemoveUserAsync(policy.OrganizationId, orgUser.Id, - savingUserId); - await _mailService.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync( - org.DisplayName(), orgUser.Email); - } - } - break; - case PolicyType.SingleOrg: - var userOrgs = await _organizationUserRepository.GetManyByManyUsersAsync( - removableOrgUsers.Select(ou => ou.UserId.Value)); - foreach (var orgUser in removableOrgUsers) - { - if (userOrgs.Any(ou => ou.UserId == orgUser.UserId - && ou.OrganizationId != org.Id - && ou.Status != OrganizationUserStatusType.Invited)) - { - await organizationService.RemoveUserAsync(policy.OrganizationId, orgUser.Id, - savingUserId); - await _mailService.SendOrganizationUserRemovedForPolicySingleOrgEmailAsync( - org.DisplayName(), orgUser.Email); - } - } - break; - default: - break; - } - } - - await SetPolicyConfiguration(policy); - } - - private async Task EnablePolicy_vNext(Policy policy, Organization org, IOrganizationService organizationService, Guid? savingUserId) + private async Task EnablePolicyAsync(Policy policy, Organization org, IOrganizationService organizationService, Guid? savingUserId) { var currentPolicy = await _policyRepository.GetByIdAsync(policy.Id); if (!currentPolicy?.Enabled ?? true) @@ -350,7 +288,7 @@ public class PolicyService : IPolicyService "Policy could not be enabled. Non-compliant members will lose access to their accounts. Identify members without two-step login from the policies column in the members page."); } - await organizationService.RemoveUserAsync(policy.OrganizationId, orgUser.Id, + await _removeOrganizationUserCommand.RemoveUserAsync(policy.OrganizationId, orgUser.Id, savingUserId); await _mailService.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync( org.DisplayName(), orgUser.Email); @@ -366,7 +304,7 @@ public class PolicyService : IPolicyService && ou.OrganizationId != org.Id && ou.Status != OrganizationUserStatusType.Invited)) { - await organizationService.RemoveUserAsync(policy.OrganizationId, orgUser.Id, + await _removeOrganizationUserCommand.RemoveUserAsync(policy.OrganizationId, orgUser.Id, savingUserId); await _mailService.SendOrganizationUserRemovedForPolicySingleOrgEmailAsync( org.DisplayName(), orgUser.Email); diff --git a/src/Core/Auth/Services/Implementations/EmergencyAccessService.cs b/src/Core/Auth/Services/Implementations/EmergencyAccessService.cs index dffa9f65c..dda16e29f 100644 --- a/src/Core/Auth/Services/Implementations/EmergencyAccessService.cs +++ b/src/Core/Auth/Services/Implementations/EmergencyAccessService.cs @@ -1,4 +1,5 @@ using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Enums; @@ -33,6 +34,7 @@ public class EmergencyAccessService : IEmergencyAccessService private readonly IPasswordHasher _passwordHasher; private readonly IOrganizationService _organizationService; private readonly IDataProtectorTokenFactory _dataProtectorTokenizer; + private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; public EmergencyAccessService( IEmergencyAccessRepository emergencyAccessRepository, @@ -46,7 +48,8 @@ public class EmergencyAccessService : IEmergencyAccessService IPasswordHasher passwordHasher, GlobalSettings globalSettings, IOrganizationService organizationService, - IDataProtectorTokenFactory dataProtectorTokenizer) + IDataProtectorTokenFactory dataProtectorTokenizer, + IRemoveOrganizationUserCommand removeOrganizationUserCommand) { _emergencyAccessRepository = emergencyAccessRepository; _organizationUserRepository = organizationUserRepository; @@ -60,6 +63,7 @@ public class EmergencyAccessService : IEmergencyAccessService _globalSettings = globalSettings; _organizationService = organizationService; _dataProtectorTokenizer = dataProtectorTokenizer; + _removeOrganizationUserCommand = removeOrganizationUserCommand; } public async Task InviteAsync(User invitingUser, string email, EmergencyAccessType type, int waitTime) @@ -341,7 +345,7 @@ public class EmergencyAccessService : IEmergencyAccessService { if (o.Type != OrganizationUserType.Owner) { - await _organizationService.RemoveUserAsync(o.OrganizationId, grantor.Id); + await _removeOrganizationUserCommand.RemoveUserAsync(o.OrganizationId, grantor.Id); } } } diff --git a/src/Core/Auth/Services/Implementations/SsoConfigService.cs b/src/Core/Auth/Services/Implementations/SsoConfigService.cs index 62c828495..fdf7e278e 100644 --- a/src/Core/Auth/Services/Implementations/SsoConfigService.cs +++ b/src/Core/Auth/Services/Implementations/SsoConfigService.cs @@ -20,7 +20,6 @@ public class SsoConfigService : ISsoConfigService private readonly IPolicyService _policyService; private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationUserRepository _organizationUserRepository; - private readonly IUserService _userService; private readonly IOrganizationService _organizationService; private readonly IEventService _eventService; @@ -30,7 +29,6 @@ public class SsoConfigService : ISsoConfigService IPolicyService policyService, IOrganizationRepository organizationRepository, IOrganizationUserRepository organizationUserRepository, - IUserService userService, IOrganizationService organizationService, IEventService eventService) { @@ -39,7 +37,6 @@ public class SsoConfigService : ISsoConfigService _policyService = policyService; _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; - _userService = userService; _organizationService = organizationService; _eventService = eventService; } @@ -74,20 +71,20 @@ public class SsoConfigService : ISsoConfigService singleOrgPolicy.Enabled = true; - await _policyService.SaveAsync(singleOrgPolicy, _userService, _organizationService, null); + await _policyService.SaveAsync(singleOrgPolicy, _organizationService, null); var resetPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(config.OrganizationId, PolicyType.ResetPassword) ?? new Policy { OrganizationId = config.OrganizationId, Type = PolicyType.ResetPassword, }; resetPolicy.Enabled = true; resetPolicy.SetDataModel(new ResetPasswordDataModel { AutoEnrollEnabled = true }); - await _policyService.SaveAsync(resetPolicy, _userService, _organizationService, null); + await _policyService.SaveAsync(resetPolicy, _organizationService, null); var ssoRequiredPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(config.OrganizationId, PolicyType.RequireSso) ?? new Policy { OrganizationId = config.OrganizationId, Type = PolicyType.RequireSso, }; ssoRequiredPolicy.Enabled = true; - await _policyService.SaveAsync(ssoRequiredPolicy, _userService, _organizationService, null); + await _policyService.SaveAsync(ssoRequiredPolicy, _organizationService, null); } await LogEventsAsync(config, oldConfig); diff --git a/src/Core/Billing/Services/IPaymentHistoryService.cs b/src/Core/Billing/Services/IPaymentHistoryService.cs index e38659b94..c2e1a7df8 100644 --- a/src/Core/Billing/Services/IPaymentHistoryService.cs +++ b/src/Core/Billing/Services/IPaymentHistoryService.cs @@ -1,4 +1,5 @@ -using Bit.Core.Billing.Models; +#nullable enable +using Bit.Core.Billing.Models; using Bit.Core.Entities; namespace Bit.Core.Billing.Services; @@ -8,7 +9,8 @@ public interface IPaymentHistoryService Task> GetInvoiceHistoryAsync( ISubscriber subscriber, int pageSize = 5, - string startAfter = null); + string? status = null, + string? startAfter = null); Task> GetTransactionHistoryAsync( ISubscriber subscriber, diff --git a/src/Core/Billing/Services/Implementations/PaymentHistoryService.cs b/src/Core/Billing/Services/Implementations/PaymentHistoryService.cs index 1e5e3ea0e..69e1a4cfb 100644 --- a/src/Core/Billing/Services/Implementations/PaymentHistoryService.cs +++ b/src/Core/Billing/Services/Implementations/PaymentHistoryService.cs @@ -1,4 +1,5 @@ -using Bit.Core.AdminConsole.Entities; +#nullable enable +using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Models; using Bit.Core.Entities; using Bit.Core.Models.BitStripe; @@ -16,11 +17,12 @@ public class PaymentHistoryService( public async Task> GetInvoiceHistoryAsync( ISubscriber subscriber, int pageSize = 5, - string startAfter = null) + string? status = null, + string? startAfter = null) { if (subscriber is not { GatewayCustomerId: not null, GatewaySubscriptionId: not null }) { - return null; + return Array.Empty(); } var invoices = await stripeAdapter.InvoiceListAsync(new StripeInvoiceListOptions @@ -28,6 +30,7 @@ public class PaymentHistoryService( Customer = subscriber.GatewayCustomerId, Subscription = subscriber.GatewaySubscriptionId, Limit = pageSize, + Status = status, StartingAfter = startAfter }); @@ -48,6 +51,7 @@ public class PaymentHistoryService( }; return transactions?.OrderByDescending(i => i.CreationDate) - .Select(t => new BillingHistoryInfo.BillingTransaction(t)); + .Select(t => new BillingHistoryInfo.BillingTransaction(t)) + ?? Array.Empty(); } } diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 1003a65b5..1fa73fcb3 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -144,6 +144,8 @@ public static class FeatureFlagKeys public const string TrialPayment = "PM-8163-trial-payment"; public const string Pm3478RefactorOrganizationUserApi = "pm-3478-refactor-organizationuser-api"; public const string RemoveServerVersionHeader = "remove-server-version-header"; + public const string AccessIntelligence = "pm-13227-access-intelligence"; + public const string VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint"; public static List GetAllKeys() { diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 735fe0758..5174543c6 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -21,8 +21,8 @@ - - + + @@ -36,7 +36,7 @@ - + @@ -46,13 +46,13 @@ - + - + diff --git a/src/Core/Models/Data/Organizations/VerifiedOrganizationDomainSsoDetail.cs b/src/Core/Models/Data/Organizations/VerifiedOrganizationDomainSsoDetail.cs new file mode 100644 index 000000000..0a07af66b --- /dev/null +++ b/src/Core/Models/Data/Organizations/VerifiedOrganizationDomainSsoDetail.cs @@ -0,0 +1,22 @@ +namespace Bit.Core.Models.Data.Organizations; + +public class VerifiedOrganizationDomainSsoDetail +{ + public VerifiedOrganizationDomainSsoDetail() + { + } + + public VerifiedOrganizationDomainSsoDetail(Guid organizationId, string organizationName, string domainName, + string organizationIdentifier) + { + OrganizationId = organizationId; + OrganizationName = organizationName; + DomainName = domainName; + OrganizationIdentifier = organizationIdentifier; + } + + public Guid OrganizationId { get; init; } + public string OrganizationName { get; init; } + public string DomainName { get; init; } + public string OrganizationIdentifier { get; init; } +} diff --git a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs index 3e2946248..58eb65faf 100644 --- a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs +++ b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs @@ -146,6 +146,7 @@ public static class OrganizationServiceCollectionExtensions services.AddScoped(); services.AddScoped(); + services.AddScoped(); } // TODO: move to OrganizationSubscriptionServiceCollectionExtensions when OrganizationUser methods are moved out of diff --git a/src/Core/Repositories/IOrganizationDomainRepository.cs b/src/Core/Repositories/IOrganizationDomainRepository.cs index 3fde08a54..f8b45574a 100644 --- a/src/Core/Repositories/IOrganizationDomainRepository.cs +++ b/src/Core/Repositories/IOrganizationDomainRepository.cs @@ -11,6 +11,7 @@ public interface IOrganizationDomainRepository : IRepository> GetDomainsByOrganizationIdAsync(Guid orgId); Task> GetManyByNextRunDateAsync(DateTime date); Task GetOrganizationDomainSsoDetailsAsync(string email); + Task> GetVerifiedOrganizationDomainSsoDetailsAsync(string email); Task GetDomainByIdOrganizationIdAsync(Guid id, Guid organizationId); Task GetDomainByOrgIdAndDomainNameAsync(Guid orgId, string domainName); Task> GetExpiredOrganizationDomainsAsync(); diff --git a/src/Core/Services/IUserService.cs b/src/Core/Services/IUserService.cs index 0135b5f1b..b15f5153e 100644 --- a/src/Core/Services/IUserService.cs +++ b/src/Core/Services/IUserService.cs @@ -40,10 +40,8 @@ public interface IUserService KdfType kdf, int kdfIterations, int? kdfMemory, int? kdfParallelism); Task RefreshSecurityStampAsync(User user, string masterPasswordHash); Task UpdateTwoFactorProviderAsync(User user, TwoFactorProviderType type, bool setEnabled = true, bool logEvent = true); - Task DisableTwoFactorProviderAsync(User user, TwoFactorProviderType type, - IOrganizationService organizationService); - Task RecoverTwoFactorAsync(string email, string masterPassword, string recoveryCode, - IOrganizationService organizationService); + Task DisableTwoFactorProviderAsync(User user, TwoFactorProviderType type); + Task RecoverTwoFactorAsync(string email, string masterPassword, string recoveryCode); Task GenerateUserTokenAsync(User user, string tokenProvider, string purpose); Task DeleteAsync(User user); Task DeleteAsync(User user, string token); diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index fe04efa22..413437a59 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -1,6 +1,7 @@ using System.Security.Claims; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Enums; @@ -65,6 +66,7 @@ public class UserService : UserManager, IUserService, IDisposable private readonly IDataProtectorTokenFactory _orgUserInviteTokenDataFactory; private readonly IFeatureService _featureService; private readonly IPremiumUserBillingService _premiumUserBillingService; + private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; public UserService( IUserRepository userRepository, @@ -98,7 +100,8 @@ public class UserService : UserManager, IUserService, IDisposable IStripeSyncService stripeSyncService, IDataProtectorTokenFactory orgUserInviteTokenDataFactory, IFeatureService featureService, - IPremiumUserBillingService premiumUserBillingService) + IPremiumUserBillingService premiumUserBillingService, + IRemoveOrganizationUserCommand removeOrganizationUserCommand) : base( store, optionsAccessor, @@ -138,6 +141,7 @@ public class UserService : UserManager, IUserService, IDisposable _orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory; _featureService = featureService; _premiumUserBillingService = premiumUserBillingService; + _removeOrganizationUserCommand = removeOrganizationUserCommand; } public Guid? GetProperUserId(ClaimsPrincipal principal) @@ -827,8 +831,7 @@ public class UserService : UserManager, IUserService, IDisposable } } - public async Task DisableTwoFactorProviderAsync(User user, TwoFactorProviderType type, - IOrganizationService organizationService) + public async Task DisableTwoFactorProviderAsync(User user, TwoFactorProviderType type) { var providers = user.GetTwoFactorProviders(); if (!providers?.ContainsKey(type) ?? true) @@ -843,12 +846,11 @@ public class UserService : UserManager, IUserService, IDisposable if (!await TwoFactorIsEnabledAsync(user)) { - await CheckPoliciesOnTwoFactorRemovalAsync(user, organizationService); + await CheckPoliciesOnTwoFactorRemovalAsync(user); } } - public async Task RecoverTwoFactorAsync(string email, string secret, string recoveryCode, - IOrganizationService organizationService) + public async Task RecoverTwoFactorAsync(string email, string secret, string recoveryCode) { var user = await _userRepository.GetByEmailAsync(email); if (user == null) @@ -872,7 +874,7 @@ public class UserService : UserManager, IUserService, IDisposable await SaveUserAsync(user); await _mailService.SendRecoverTwoFactorEmail(user.Email, DateTime.UtcNow, _currentContext.IpAddress); await _eventService.LogUserEventAsync(user.Id, EventType.User_Recovered2fa); - await CheckPoliciesOnTwoFactorRemovalAsync(user, organizationService); + await CheckPoliciesOnTwoFactorRemovalAsync(user); return true; } @@ -1327,13 +1329,13 @@ public class UserService : UserManager, IUserService, IDisposable } } - private async Task CheckPoliciesOnTwoFactorRemovalAsync(User user, IOrganizationService organizationService) + private async Task CheckPoliciesOnTwoFactorRemovalAsync(User user) { var twoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication); var removeOrgUserTasks = twoFactorPolicies.Select(async p => { - await organizationService.RemoveUserAsync(p.OrganizationId, user.Id); + await _removeOrganizationUserCommand.RemoveUserAsync(p.OrganizationId, user.Id); var organization = await _organizationRepository.GetByIdAsync(p.OrganizationId); await _mailService.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync( organization.DisplayName(), user.Email); diff --git a/src/Identity/IdentityServer/BaseRequestValidator.cs b/src/Identity/IdentityServer/BaseRequestValidator.cs index 881ae4d49..8129a1a10 100644 --- a/src/Identity/IdentityServer/BaseRequestValidator.cs +++ b/src/Identity/IdentityServer/BaseRequestValidator.cs @@ -1,6 +1,4 @@ -using System.ComponentModel.DataAnnotations; -using System.Reflection; -using System.Security.Claims; +using System.Security.Claims; using System.Text.Json; using Bit.Core; using Bit.Core.AdminConsole.Entities; @@ -33,9 +31,8 @@ namespace Bit.Identity.IdentityServer; public abstract class BaseRequestValidator where T : class { private UserManager _userManager; - private readonly IDeviceRepository _deviceRepository; - private readonly IDeviceService _deviceService; private readonly IEventService _eventService; + private readonly IDeviceValidator _deviceValidator; private readonly IOrganizationDuoWebTokenProvider _organizationDuoWebTokenProvider; private readonly ITemporaryDuoWebV4SDKService _duoWebV4SDKService; private readonly IOrganizationRepository _organizationRepository; @@ -56,10 +53,9 @@ public abstract class BaseRequestValidator where T : class public BaseRequestValidator( UserManager userManager, - IDeviceRepository deviceRepository, - IDeviceService deviceService, IUserService userService, IEventService eventService, + IDeviceValidator deviceValidator, IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider, ITemporaryDuoWebV4SDKService duoWebV4SDKService, IOrganizationRepository organizationRepository, @@ -77,10 +73,9 @@ public abstract class BaseRequestValidator where T : class IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder) { _userManager = userManager; - _deviceRepository = deviceRepository; - _deviceService = deviceService; _userService = userService; _eventService = eventService; + _deviceValidator = deviceValidator; _organizationDuoWebTokenProvider = organizationDuoWebTokenProvider; _duoWebV4SDKService = duoWebV4SDKService; _organizationRepository = organizationRepository; @@ -131,9 +126,7 @@ public abstract class BaseRequestValidator where T : class var (isTwoFactorRequired, twoFactorOrganization) = await RequiresTwoFactorAsync(user, request); if (isTwoFactorRequired) { - // Just defaulting it - var twoFactorProviderType = TwoFactorProviderType.Authenticator; - if (!twoFactorRequest || !Enum.TryParse(twoFactorProvider, out twoFactorProviderType)) + if (!twoFactorRequest || !Enum.TryParse(twoFactorProvider, out TwoFactorProviderType twoFactorProviderType)) { await BuildTwoFactorResultAsync(user, twoFactorOrganization, context); return; @@ -162,7 +155,6 @@ public abstract class BaseRequestValidator where T : class twoFactorToken = null; } - // Force legacy users to the web for migration if (FeatureService.IsEnabled(FeatureFlagKeys.BlockLegacyUsers)) { @@ -176,7 +168,7 @@ public abstract class BaseRequestValidator where T : class // Returns true if can finish validation process if (await IsValidAuthTypeAsync(user, request.GrantType)) { - var device = await SaveDeviceAsync(user, request); + var device = await _deviceValidator.SaveDeviceAsync(user, request); if (device == null) { await BuildErrorResultAsync("No device information provided.", false, context, user); @@ -393,28 +385,6 @@ public abstract class BaseRequestValidator where T : class orgAbilities[orgId].Enabled && orgAbilities[orgId].Using2fa; } - private Device GetDeviceFromRequest(ValidatedRequest request) - { - var deviceIdentifier = request.Raw["DeviceIdentifier"]?.ToString(); - var deviceType = request.Raw["DeviceType"]?.ToString(); - var deviceName = request.Raw["DeviceName"]?.ToString(); - var devicePushToken = request.Raw["DevicePushToken"]?.ToString(); - - if (string.IsNullOrWhiteSpace(deviceIdentifier) || string.IsNullOrWhiteSpace(deviceType) || - string.IsNullOrWhiteSpace(deviceName) || !Enum.TryParse(deviceType, out DeviceType type)) - { - return null; - } - - return new Device - { - Identifier = deviceIdentifier, - Name = deviceName, - Type = type, - PushToken = string.IsNullOrWhiteSpace(devicePushToken) ? null : devicePushToken - }; - } - private async Task VerifyTwoFactor(User user, Organization organization, TwoFactorProviderType type, string token) { @@ -537,51 +507,6 @@ public abstract class BaseRequestValidator where T : class } } - protected async Task KnownDeviceAsync(User user, ValidatedTokenRequest request) => - (await GetKnownDeviceAsync(user, request)) != default; - - protected async Task GetKnownDeviceAsync(User user, ValidatedTokenRequest request) - { - if (user == null) - { - return default; - } - - return await _deviceRepository.GetByIdentifierAsync(GetDeviceFromRequest(request).Identifier, user.Id); - } - - private async Task SaveDeviceAsync(User user, ValidatedTokenRequest request) - { - var device = GetDeviceFromRequest(request); - if (device != null) - { - var existingDevice = await GetKnownDeviceAsync(user, request); - if (existingDevice == null) - { - device.UserId = user.Id; - await _deviceService.SaveAsync(device); - - var now = DateTime.UtcNow; - if (now - user.CreationDate > TimeSpan.FromMinutes(10)) - { - var deviceType = device.Type.GetType().GetMember(device.Type.ToString()) - .FirstOrDefault()?.GetCustomAttribute()?.GetName(); - if (!_globalSettings.DisableEmailNewDevice) - { - await _mailService.SendNewDeviceLoggedInEmail(user.Email, deviceType, now, - CurrentContext.IpAddress); - } - } - - return device; - } - - return existingDevice; - } - - return null; - } - private async Task ResetFailedAuthDetailsAsync(User user) { // Early escape if db hit not necessary diff --git a/src/Identity/IdentityServer/CustomTokenRequestValidator.cs b/src/Identity/IdentityServer/CustomTokenRequestValidator.cs index 3af1337ee..0d7a92c8a 100644 --- a/src/Identity/IdentityServer/CustomTokenRequestValidator.cs +++ b/src/Identity/IdentityServer/CustomTokenRequestValidator.cs @@ -29,8 +29,7 @@ public class CustomTokenRequestValidator : BaseRequestValidator userManager, - IDeviceRepository deviceRepository, - IDeviceService deviceService, + IDeviceValidator deviceValidator, IUserService userService, IEventService eventService, IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider, @@ -48,7 +47,7 @@ public class CustomTokenRequestValidator : BaseRequestValidator tokenDataFactory, IFeatureService featureService, IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder) - : base(userManager, deviceRepository, deviceService, userService, eventService, + : base(userManager, userService, eventService, deviceValidator, organizationDuoWebTokenProvider, duoWebV4SDKService, organizationRepository, organizationUserRepository, applicationCacheService, mailService, logger, currentContext, globalSettings, userRepository, policyService, tokenDataFactory, featureService, ssoConfigRepository, @@ -83,11 +82,8 @@ public class CustomTokenRequestValidator : BaseRequestValidator { { "encrypted_payload", payload } }; } - - return; } - await ValidateAsync(context, context.Result.ValidatedRequest, new CustomValidatorRequestContext { KnownDevice = true }); } @@ -103,7 +99,6 @@ public class CustomTokenRequestValidator : BaseRequestValidator + /// Save a device to the database. If the device is already known, it will be returned. + /// + /// The user is assumed NOT null, still going to check though + /// Duende Validated Request that contains the data to create the device object + /// Returns null if user or device is malformed; The existing device if already in DB; a new device login + Task SaveDeviceAsync(User user, ValidatedTokenRequest request); + /// + /// Check if a device is known to the user. + /// + /// current user trying to authenticate + /// contains raw information that is parsed about the device + /// true if the device is known, false if it is not + Task KnownDeviceAsync(User user, ValidatedTokenRequest request); +} + +public class DeviceValidator( + IDeviceService deviceService, + IDeviceRepository deviceRepository, + GlobalSettings globalSettings, + IMailService mailService, + ICurrentContext currentContext) : IDeviceValidator +{ + private readonly IDeviceService _deviceService = deviceService; + private readonly IDeviceRepository _deviceRepository = deviceRepository; + private readonly GlobalSettings _globalSettings = globalSettings; + private readonly IMailService _mailService = mailService; + private readonly ICurrentContext _currentContext = currentContext; + + public async Task SaveDeviceAsync(User user, ValidatedTokenRequest request) + { + var device = GetDeviceFromRequest(request); + if (device != null && user != null) + { + var existingDevice = await GetKnownDeviceAsync(user, device); + if (existingDevice == null) + { + device.UserId = user.Id; + await _deviceService.SaveAsync(device); + + // This makes sure the user isn't sent a "new device" email on their first login + var now = DateTime.UtcNow; + if (now - user.CreationDate > TimeSpan.FromMinutes(10)) + { + var deviceType = device.Type.GetType().GetMember(device.Type.ToString()) + .FirstOrDefault()?.GetCustomAttribute()?.GetName(); + if (!_globalSettings.DisableEmailNewDevice) + { + await _mailService.SendNewDeviceLoggedInEmail(user.Email, deviceType, now, + _currentContext.IpAddress); + } + } + return device; + } + return existingDevice; + } + return null; + } + + public async Task KnownDeviceAsync(User user, ValidatedTokenRequest request) => + (await GetKnownDeviceAsync(user, GetDeviceFromRequest(request))) != default; + + private async Task GetKnownDeviceAsync(User user, Device device) + { + if (user == null || device == null) + { + return default; + } + return await _deviceRepository.GetByIdentifierAsync(device.Identifier, user.Id); + } + + private static Device GetDeviceFromRequest(ValidatedRequest request) + { + var deviceIdentifier = request.Raw["DeviceIdentifier"]?.ToString(); + var requestDeviceType = request.Raw["DeviceType"]?.ToString(); + var deviceName = request.Raw["DeviceName"]?.ToString(); + var devicePushToken = request.Raw["DevicePushToken"]?.ToString(); + + if (string.IsNullOrWhiteSpace(deviceIdentifier) || + string.IsNullOrWhiteSpace(requestDeviceType) || + string.IsNullOrWhiteSpace(deviceName) || + !Enum.TryParse(requestDeviceType, out DeviceType parsedDeviceType)) + { + return null; + } + + return new Device + { + Identifier = deviceIdentifier, + Name = deviceName, + Type = parsedDeviceType, + PushToken = string.IsNullOrWhiteSpace(devicePushToken) ? null : devicePushToken + }; + } +} diff --git a/src/Identity/IdentityServer/ResourceOwnerPasswordValidator.cs b/src/Identity/IdentityServer/ResourceOwnerPasswordValidator.cs index cb63bd94e..08560e240 100644 --- a/src/Identity/IdentityServer/ResourceOwnerPasswordValidator.cs +++ b/src/Identity/IdentityServer/ResourceOwnerPasswordValidator.cs @@ -25,12 +25,12 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator userManager, - IDeviceRepository deviceRepository, - IDeviceService deviceService, IUserService userService, IEventService eventService, + IDeviceValidator deviceValidator, IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider, ITemporaryDuoWebV4SDKService duoWebV4SDKService, IOrganizationRepository organizationRepository, @@ -48,7 +48,7 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator _assertionOptionsDataProtector; private readonly IAssertWebAuthnLoginCredentialCommand _assertWebAuthnLoginCredentialCommand; + private readonly IDeviceValidator _deviceValidator; public WebAuthnGrantValidator( UserManager userManager, - IDeviceRepository deviceRepository, - IDeviceService deviceService, IUserService userService, IEventService eventService, + IDeviceValidator deviceValidator, IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider, ITemporaryDuoWebV4SDKService duoWebV4SDKService, IOrganizationRepository organizationRepository, @@ -52,13 +52,14 @@ public class WebAuthnGrantValidator : BaseRequestValidator "webauthn"; @@ -87,7 +88,7 @@ public class WebAuthnGrantValidator : BaseRequestValidator(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); var issuerUri = new Uri(globalSettings.BaseServiceUri.InternalIdentity); var identityServerBuilder = services diff --git a/src/Infrastructure.Dapper/Repositories/OrganizationDomainRepository.cs b/src/Infrastructure.Dapper/Repositories/OrganizationDomainRepository.cs index 31d599f0c..1a7085eb1 100644 --- a/src/Infrastructure.Dapper/Repositories/OrganizationDomainRepository.cs +++ b/src/Infrastructure.Dapper/Repositories/OrganizationDomainRepository.cs @@ -71,6 +71,17 @@ public class OrganizationDomainRepository : Repository } } + public async Task> GetVerifiedOrganizationDomainSsoDetailsAsync(string email) + { + await using var connection = new SqlConnection(ConnectionString); + + return await connection + .QueryAsync( + $"[{Schema}].[VerifiedOrganizationDomainSsoDetails_ReadByEmail]", + new { Email = email }, + commandType: CommandType.StoredProcedure); + } + public async Task GetDomainByIdOrganizationIdAsync(Guid id, Guid orgId) { using (var connection = new SqlConnection(ConnectionString)) diff --git a/src/Infrastructure.EntityFramework/Repositories/OrganizationDomainRepository.cs b/src/Infrastructure.EntityFramework/Repositories/OrganizationDomainRepository.cs index 9135c8bd1..3e2d6e44a 100644 --- a/src/Infrastructure.EntityFramework/Repositories/OrganizationDomainRepository.cs +++ b/src/Infrastructure.EntityFramework/Repositories/OrganizationDomainRepository.cs @@ -95,6 +95,29 @@ public class OrganizationDomainRepository : Repository> GetVerifiedOrganizationDomainSsoDetailsAsync(string email) + { + var domainName = new MailAddress(email).Host; + + using var scope = ServiceScopeFactory.CreateScope(); + var dbContext = GetDatabaseContext(scope); + return await (from o in dbContext.Organizations + from od in o.Domains + join s in dbContext.SsoConfigs on o.Id equals s.OrganizationId into sJoin + from s in sJoin.DefaultIfEmpty() + where od.DomainName == domainName + && o.Enabled + && s.Enabled + && od.VerifiedDate != null + select new VerifiedOrganizationDomainSsoDetail( + o.Id, + o.Name, + od.DomainName, + o.Identifier)) + .AsNoTracking() + .ToListAsync(); + } + public async Task GetDomainByIdOrganizationIdAsync(Guid id, Guid orgId) { using var scope = ServiceScopeFactory.CreateScope(); diff --git a/src/SharedWeb/SharedWeb.csproj b/src/SharedWeb/SharedWeb.csproj index a4c500d24..40915cb7e 100644 --- a/src/SharedWeb/SharedWeb.csproj +++ b/src/SharedWeb/SharedWeb.csproj @@ -7,7 +7,7 @@ - + diff --git a/src/SharedWeb/Utilities/RequestLoggingMiddleware.cs b/src/SharedWeb/Utilities/RequestLoggingMiddleware.cs index 4fb0e8f92..77efdbfcf 100644 --- a/src/SharedWeb/Utilities/RequestLoggingMiddleware.cs +++ b/src/SharedWeb/Utilities/RequestLoggingMiddleware.cs @@ -38,7 +38,8 @@ public sealed class RequestLoggingMiddleware new RequestLogScope(context.GetIpAddress(_globalSettings), GetHeaderValue(context, "user-agent"), GetHeaderValue(context, "device-type"), - GetHeaderValue(context, "device-type")))) + GetHeaderValue(context, "device-type"), + GetHeaderValue(context, "bitwarden-client-version")))) { return _next(context); } @@ -59,12 +60,13 @@ public sealed class RequestLoggingMiddleware { private string? _cachedToString; - public RequestLogScope(string? ipAddress, string? userAgent, string? deviceType, string? origin) + public RequestLogScope(string? ipAddress, string? userAgent, string? deviceType, string? origin, string? clientVersion) { IpAddress = ipAddress; UserAgent = userAgent; DeviceType = deviceType; Origin = origin; + ClientVersion = clientVersion; } public KeyValuePair this[int index] @@ -87,17 +89,22 @@ public sealed class RequestLoggingMiddleware { return new KeyValuePair(nameof(Origin), Origin); } + else if (index == 4) + { + return new KeyValuePair(nameof(ClientVersion), ClientVersion); + } throw new ArgumentOutOfRangeException(nameof(index)); } } - public int Count => 4; + public int Count => 5; public string? IpAddress { get; } public string? UserAgent { get; } public string? DeviceType { get; } public string? Origin { get; } + public string? ClientVersion { get; } public IEnumerator> GetEnumerator() { @@ -110,7 +117,7 @@ public sealed class RequestLoggingMiddleware public override string ToString() { - _cachedToString ??= $"IpAddress:{IpAddress} UserAgent:{UserAgent} DeviceType:{DeviceType} Origin:{Origin}"; + _cachedToString ??= $"IpAddress:{IpAddress} UserAgent:{UserAgent} DeviceType:{DeviceType} Origin:{Origin} ClientVersion:{ClientVersion}"; return _cachedToString; } } diff --git a/src/Sql/dbo/Stored Procedures/VerfiedOrganaizationDomainSsoDetails_ReadByEmail.sql b/src/Sql/dbo/Stored Procedures/VerfiedOrganaizationDomainSsoDetails_ReadByEmail.sql new file mode 100644 index 000000000..a32b42f6c --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/VerfiedOrganaizationDomainSsoDetails_ReadByEmail.sql @@ -0,0 +1,23 @@ +CREATE PROCEDURE [dbo].[VerifiedOrganizationDomainSsoDetails_ReadByEmail] +@Email NVARCHAR(256) +AS +BEGIN + SET NOCOUNT ON + + DECLARE @Domain NVARCHAR(256) + + SELECT @Domain = SUBSTRING(@Email, CHARINDEX( '@', @Email) + 1, LEN(@Email)) + + SELECT + O.Id AS OrganizationId, + O.Name AS OrganizationName, + O.Identifier AS OrganizationIdentifier, + OD.DomainName + FROM [dbo].[OrganizationView] O + INNER JOIN [dbo].[OrganizationDomainView] OD ON O.Id = OD.OrganizationId + LEFT JOIN [dbo].[Ssoconfig] S ON O.Id = S.OrganizationId + WHERE OD.DomainName = @Domain + AND O.Enabled = 1 + AND OD.VerifiedDate IS NOT NULL + AND S.Enabled = 1 +END diff --git a/test/Api.Test/AdminConsole/Controllers/OrganizationDomainControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/OrganizationDomainControllerTests.cs index 2f7430341..1ff4b519c 100644 --- a/test/Api.Test/AdminConsole/Controllers/OrganizationDomainControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/OrganizationDomainControllerTests.cs @@ -316,4 +316,26 @@ public class OrganizationDomainControllerTests Assert.IsType(result); } + + [Theory, BitAutoData] + public async Task GetVerifiedOrgDomainSsoDetails_ShouldThrowNotFound_WhenEmailHasNotClaimedDomain( + OrganizationDomainSsoDetailsRequestModel model, SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetVerifiedOrganizationDomainSsoDetailsAsync(model.Email).Returns(Array.Empty()); + + await Assert.ThrowsAsync(() => sutProvider.Sut.GetOrgDomainSsoDetails(model)); + } + + [Theory, BitAutoData] + public async Task GetVerifiedOrgDomainSsoDetails_ShouldReturnOrganizationDomainSsoDetails_WhenEmailHasClaimedDomain( + OrganizationDomainSsoDetailsRequestModel model, IEnumerable ssoDetailsData, SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetVerifiedOrganizationDomainSsoDetailsAsync(model.Email).Returns(ssoDetailsData); + + var result = await sutProvider.Sut.GetVerifiedOrgDomainSsoDetailsAsync(model); + + Assert.IsType(result); + } } diff --git a/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs index 156df1476..25227fec7 100644 --- a/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs @@ -7,6 +7,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Models.Business.Tokenables; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Enums; @@ -49,6 +50,7 @@ public class OrganizationsControllerTests : IDisposable private readonly IProviderRepository _providerRepository; private readonly IProviderBillingService _providerBillingService; private readonly IDataProtectorTokenFactory _orgDeleteTokenDataFactory; + private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; private readonly OrganizationsController _sut; @@ -72,6 +74,7 @@ public class OrganizationsControllerTests : IDisposable _providerRepository = Substitute.For(); _providerBillingService = Substitute.For(); _orgDeleteTokenDataFactory = Substitute.For>(); + _removeOrganizationUserCommand = Substitute.For(); _sut = new OrganizationsController( _organizationRepository, @@ -91,7 +94,8 @@ public class OrganizationsControllerTests : IDisposable _pushNotificationService, _providerRepository, _providerBillingService, - _orgDeleteTokenDataFactory); + _orgDeleteTokenDataFactory, + _removeOrganizationUserCommand); } public void Dispose() @@ -120,13 +124,12 @@ public class OrganizationsControllerTests : IDisposable _ssoConfigRepository.GetByOrganizationIdAsync(orgId).Returns(ssoConfig); _userService.GetUserByPrincipalAsync(Arg.Any()).Returns(user); - var exception = await Assert.ThrowsAsync( - () => _sut.Leave(orgId.ToString())); + var exception = await Assert.ThrowsAsync(() => _sut.Leave(orgId)); Assert.Contains("Your organization's Single Sign-On settings prevent you from leaving.", exception.Message); - await _organizationService.DidNotReceiveWithAnyArgs().RemoveUserAsync(default, default); + await _removeOrganizationUserCommand.DidNotReceiveWithAnyArgs().RemoveUserAsync(default, default); } [Theory] @@ -155,8 +158,9 @@ public class OrganizationsControllerTests : IDisposable _ssoConfigRepository.GetByOrganizationIdAsync(orgId).Returns(ssoConfig); _userService.GetUserByPrincipalAsync(Arg.Any()).Returns(user); - await _organizationService.RemoveUserAsync(orgId, user.Id); - await _organizationService.Received(1).RemoveUserAsync(orgId, user.Id); + await _sut.Leave(orgId); + + await _removeOrganizationUserCommand.Received(1).RemoveUserAsync(orgId, user.Id); } [Theory, AutoData] diff --git a/test/Api.Test/Billing/Controllers/OrganizationsControllerTests.cs b/test/Api.Test/Billing/Controllers/OrganizationsControllerTests.cs index 8fb648e89..ec6047fbf 100644 --- a/test/Api.Test/Billing/Controllers/OrganizationsControllerTests.cs +++ b/test/Api.Test/Billing/Controllers/OrganizationsControllerTests.cs @@ -4,8 +4,8 @@ using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.Billing.Controllers; using Bit.Api.Models.Request.Organizations; using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.Repositories; -using Bit.Core.Auth.Entities; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.Repositories; @@ -46,6 +46,7 @@ public class OrganizationsControllerTests : IDisposable private readonly IAddSecretsManagerSubscriptionCommand _addSecretsManagerSubscriptionCommand; private readonly IReferenceEventService _referenceEventService; private readonly ISubscriberService _subscriberService; + private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; private readonly OrganizationsController _sut; @@ -68,6 +69,7 @@ public class OrganizationsControllerTests : IDisposable _addSecretsManagerSubscriptionCommand = Substitute.For(); _referenceEventService = Substitute.For(); _subscriberService = Substitute.For(); + _removeOrganizationUserCommand = Substitute.For(); _sut = new OrganizationsController( _organizationRepository, @@ -91,36 +93,6 @@ public class OrganizationsControllerTests : IDisposable _sut?.Dispose(); } - [Theory] - [InlineAutoData(true, false)] - [InlineAutoData(false, true)] - [InlineAutoData(false, false)] - public async Task OrganizationsController_UserCanLeaveOrganizationThatDoesntProvideKeyConnector( - bool keyConnectorEnabled, bool userUsesKeyConnector, Guid orgId, User user) - { - var ssoConfig = new SsoConfig - { - Id = default, - Data = new SsoConfigurationData - { - MemberDecryptionType = keyConnectorEnabled - ? MemberDecryptionType.KeyConnector - : MemberDecryptionType.MasterPassword - }.Serialize(), - Enabled = true, - OrganizationId = orgId, - }; - - user.UsesKeyConnector = userUsesKeyConnector; - - _currentContext.OrganizationUser(orgId).Returns(true); - _ssoConfigRepository.GetByOrganizationIdAsync(orgId).Returns(ssoConfig); - _userService.GetUserByPrincipalAsync(Arg.Any()).Returns(user); - - await _organizationService.RemoveUserAsync(orgId, user.Id); - await _organizationService.Received(1).RemoveUserAsync(orgId, user.Id); - } - [Theory, AutoData] public async Task OrganizationsController_PostUpgrade_UserCannotEditSubscription_ThrowsNotFoundException( Guid organizationId, diff --git a/test/Common/Common.csproj b/test/Common/Common.csproj index 1893487d2..7b9fbe42d 100644 --- a/test/Common/Common.csproj +++ b/test/Common/Common.csproj @@ -14,7 +14,7 @@ - + diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteManagedOrganizationUserAccountCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteManagedOrganizationUserAccountCommandTests.cs index 585c5fc8d..81e83d745 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteManagedOrganizationUserAccountCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteManagedOrganizationUserAccountCommandTests.cs @@ -40,7 +40,7 @@ public class DeleteManagedOrganizationUserAccountCommandTests Arg.Is>(ids => ids.Contains(organizationUser.Id))) .Returns(new Dictionary { { organizationUser.Id, true } }); - sutProvider.GetDependency() + sutProvider.GetDependency() .HasConfirmedOwnersExceptAsync( organizationUser.OrganizationId, Arg.Is>(ids => ids.Contains(organizationUser.Id)), @@ -184,7 +184,7 @@ public class DeleteManagedOrganizationUserAccountCommandTests .OrganizationOwner(organizationUser.OrganizationId) .Returns(true); - sutProvider.GetDependency() + sutProvider.GetDependency() .HasConfirmedOwnersExceptAsync( organizationUser.OrganizationId, Arg.Is>(ids => ids.Contains(organizationUser.Id)), @@ -399,8 +399,8 @@ public class DeleteManagedOrganizationUserAccountCommandTests .OrganizationOwner(orgUser.OrganizationId) .Returns(true); - sutProvider.GetDependency() - .HasConfirmedOwnersExceptAsync(orgUser.OrganizationId, Arg.Any>(), true) + sutProvider.GetDependency() + .HasConfirmedOwnersExceptAsync(orgUser.OrganizationId, Arg.Any>(), Arg.Any()) .Returns(false); // Act diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/HasConfirmedOwnersExceptQueryTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/HasConfirmedOwnersExceptQueryTests.cs new file mode 100644 index 000000000..77e99a8e2 --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/HasConfirmedOwnersExceptQueryTests.cs @@ -0,0 +1,139 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Entities.Provider; +using Bit.Core.AdminConsole.Enums.Provider; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Repositories; +using Bit.Core.Test.AutoFixture.OrganizationUserFixtures; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers; + +[SutProviderCustomize] +public class HasConfirmedOwnersExceptQueryTests +{ + [Theory, BitAutoData] + public async Task HasConfirmedOwnersExcept_WithConfirmedOwner_WithNoException_ReturnsTrue( + Organization organization, + [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner, + SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetManyByOrganizationAsync(organization.Id, OrganizationUserType.Owner) + .Returns(new List { owner }); + + var result = await sutProvider.Sut.HasConfirmedOwnersExceptAsync(organization.Id, new List(), true); + + Assert.True(result); + } + + [Theory, BitAutoData] + public async Task HasConfirmedOwnersExcept_ExcludingConfirmedOwner_ReturnsFalse( + Organization organization, + [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner, + SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetManyByOrganizationAsync(organization.Id, OrganizationUserType.Owner) + .Returns(new List { owner }); + + var result = await sutProvider.Sut.HasConfirmedOwnersExceptAsync(organization.Id, new List { owner.Id }, true); + + Assert.False(result); + } + + [Theory, BitAutoData] + public async Task HasConfirmedOwnersExcept_WithInvitedOwner_ReturnsFalse( + Organization organization, + [OrganizationUser(OrganizationUserStatusType.Invited, OrganizationUserType.Owner)] OrganizationUser owner, + SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetManyByOrganizationAsync(organization.Id, OrganizationUserType.Owner) + .Returns(new List { owner }); + + var result = await sutProvider.Sut.HasConfirmedOwnersExceptAsync(organization.Id, new List(), true); + + Assert.False(result); + } + + [Theory] + [BitAutoData(true)] + [BitAutoData(false)] + public async Task HasConfirmedOwnersExcept_WithConfirmedProviderUser_IncludeProviderTrue_ReturnsTrue( + bool includeProvider, + Organization organization, + ProviderUser providerUser, + SutProvider sutProvider) + { + providerUser.Status = ProviderUserStatusType.Confirmed; + + sutProvider.GetDependency() + .GetManyByOrganizationAsync(organization.Id, ProviderUserStatusType.Confirmed) + .Returns(new List { providerUser }); + + var result = await sutProvider.Sut.HasConfirmedOwnersExceptAsync(organization.Id, new List(), includeProvider); + + Assert.Equal(includeProvider, result); + } + + [Theory, BitAutoData] + public async Task HasConfirmedOwnersExceptAsync_WithConfirmedOwners_ReturnsTrue( + Guid organizationId, + IEnumerable organizationUsersId, + ICollection owners, + SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetManyByOrganizationAsync(organizationId, OrganizationUserType.Owner) + .Returns(owners); + + var result = await sutProvider.Sut.HasConfirmedOwnersExceptAsync(organizationId, organizationUsersId); + + Assert.True(result); + } + + [Theory, BitAutoData] + public async Task HasConfirmedOwnersExceptAsync_WithConfirmedProviders_ReturnsTrue( + Guid organizationId, + IEnumerable organizationUsersId, + ICollection providerUsers, + SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetManyByOrganizationAsync(organizationId, OrganizationUserType.Owner) + .Returns(new List()); + + sutProvider.GetDependency() + .GetManyByOrganizationAsync(organizationId, ProviderUserStatusType.Confirmed) + .Returns(providerUsers); + + var result = await sutProvider.Sut.HasConfirmedOwnersExceptAsync(organizationId, organizationUsersId); + + Assert.True(result); + } + + [Theory, BitAutoData] + public async Task HasConfirmedOwnersExceptAsync_WithNoConfirmedOwnersOrProviders_ReturnsFalse( + Guid organizationId, + IEnumerable organizationUsersId, + SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetManyByOrganizationAsync(organizationId, OrganizationUserType.Owner) + .Returns(new List()); + + sutProvider.GetDependency() + .GetManyByOrganizationAsync(organizationId, ProviderUserStatusType.Confirmed) + .Returns(new List()); + + var result = await sutProvider.Sut.HasConfirmedOwnersExceptAsync(organizationId, organizationUsersId); + + Assert.False(result); + } +} diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommandTests.cs index 8d25f13cb..2d10ce626 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommandTests.cs @@ -1,9 +1,12 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Repositories; using Bit.Core.Services; +using Bit.Core.Test.AutoFixture.OrganizationUserFixtures; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; @@ -14,33 +17,38 @@ namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers; [SutProviderCustomize] public class RemoveOrganizationUserCommandTests { - [Theory] - [BitAutoData] - public async Task RemoveUser_Success(SutProvider sutProvider, Guid organizationId, Guid organizationUserId) + [Theory, BitAutoData] + public async Task RemoveUser_Success( + [OrganizationUser(type: OrganizationUserType.User)] OrganizationUser organizationUser, + [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser deletingUser, + SutProvider sutProvider) { - sutProvider.GetDependency() - .GetByIdAsync(organizationUserId) - .Returns(new OrganizationUser - { - Id = organizationUserId, - OrganizationId = organizationId - }); + var organizationUserRepository = sutProvider.GetDependency(); + var currentContext = sutProvider.GetDependency(); - await sutProvider.Sut.RemoveUserAsync(organizationId, organizationUserId, null); + organizationUser.OrganizationId = deletingUser.OrganizationId; + organizationUserRepository.GetByIdAsync(organizationUser.Id).Returns(organizationUser); + organizationUserRepository.GetByIdAsync(deletingUser.Id).Returns(deletingUser); + currentContext.OrganizationOwner(deletingUser.OrganizationId).Returns(true); - await sutProvider.GetDependency().Received(1).RemoveUserAsync(organizationId, organizationUserId, null); + await sutProvider.Sut.RemoveUserAsync(deletingUser.OrganizationId, organizationUser.Id, deletingUser.UserId); + + await organizationUserRepository.Received(1).DeleteAsync(organizationUser); + await sutProvider.GetDependency().Received(1).LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed); } [Theory] [BitAutoData] - public async Task RemoveUser_NotFound_Throws(SutProvider sutProvider, Guid organizationId, Guid organizationUserId) + public async Task RemoveUser_NotFound_ThrowsException(SutProvider sutProvider, + Guid organizationId, Guid organizationUserId) { await Assert.ThrowsAsync(async () => await sutProvider.Sut.RemoveUserAsync(organizationId, organizationUserId, null)); } [Theory] [BitAutoData] - public async Task RemoveUser_MismatchingOrganizationId_Throws(SutProvider sutProvider, Guid organizationId, Guid organizationUserId) + public async Task RemoveUser_MismatchingOrganizationId_ThrowsException( + SutProvider sutProvider, Guid organizationId, Guid organizationUserId) { sutProvider.GetDependency() .GetByIdAsync(organizationUserId) @@ -53,20 +61,249 @@ public class RemoveOrganizationUserCommandTests await Assert.ThrowsAsync(async () => await sutProvider.Sut.RemoveUserAsync(organizationId, organizationUserId, null)); } - [Theory] - [BitAutoData] - public async Task RemoveUser_WithEventSystemUser_Success(SutProvider sutProvider, Guid organizationId, Guid organizationUserId, EventSystemUser eventSystemUser) + [Theory, BitAutoData] + public async Task RemoveUser_InvalidUser_ThrowsException( + OrganizationUser organizationUser, OrganizationUser deletingUser, + SutProvider sutProvider) { - sutProvider.GetDependency() - .GetByIdAsync(organizationUserId) - .Returns(new OrganizationUser - { - Id = organizationUserId, - OrganizationId = organizationId - }); + var organizationUserRepository = sutProvider.GetDependency(); - await sutProvider.Sut.RemoveUserAsync(organizationId, organizationUserId, eventSystemUser); + organizationUserRepository.GetByIdAsync(organizationUser.Id).Returns(organizationUser); - await sutProvider.GetDependency().Received(1).RemoveUserAsync(organizationId, organizationUserId, eventSystemUser); + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.RemoveUserAsync(Guid.NewGuid(), organizationUser.Id, deletingUser.UserId)); + Assert.Contains("User not found.", exception.Message); + } + + [Theory, BitAutoData] + public async Task RemoveUser_RemoveYourself_ThrowsException(OrganizationUser deletingUser, SutProvider sutProvider) + { + var organizationUserRepository = sutProvider.GetDependency(); + + organizationUserRepository.GetByIdAsync(deletingUser.Id).Returns(deletingUser); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.RemoveUserAsync(deletingUser.OrganizationId, deletingUser.Id, deletingUser.UserId)); + Assert.Contains("You cannot remove yourself.", exception.Message); + } + + [Theory, BitAutoData] + public async Task RemoveUser_NonOwnerRemoveOwner_ThrowsException( + [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser organizationUser, + [OrganizationUser(type: OrganizationUserType.Admin)] OrganizationUser deletingUser, + SutProvider sutProvider) + { + var organizationUserRepository = sutProvider.GetDependency(); + var currentContext = sutProvider.GetDependency(); + + organizationUser.OrganizationId = deletingUser.OrganizationId; + organizationUserRepository.GetByIdAsync(organizationUser.Id).Returns(organizationUser); + currentContext.OrganizationAdmin(deletingUser.OrganizationId).Returns(true); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.RemoveUserAsync(deletingUser.OrganizationId, organizationUser.Id, deletingUser.UserId)); + Assert.Contains("Only owners can delete other owners.", exception.Message); + } + + [Theory, BitAutoData] + public async Task RemoveUser_RemovingLastOwner_ThrowsException( + [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser organizationUser, + OrganizationUser deletingUser, + SutProvider sutProvider) + { + var organizationUserRepository = sutProvider.GetDependency(); + var hasConfirmedOwnersExceptQuery = sutProvider.GetDependency(); + + organizationUser.OrganizationId = deletingUser.OrganizationId; + organizationUserRepository.GetByIdAsync(organizationUser.Id).Returns(organizationUser); + hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync( + deletingUser.OrganizationId, + Arg.Is>(i => i.Contains(organizationUser.Id)), Arg.Any()) + .Returns(false); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.RemoveUserAsync(deletingUser.OrganizationId, organizationUser.Id, null)); + Assert.Contains("Organization must have at least one confirmed owner.", exception.Message); + hasConfirmedOwnersExceptQuery + .Received(1) + .HasConfirmedOwnersExceptAsync( + organizationUser.OrganizationId, + Arg.Is>(i => i.Contains(organizationUser.Id)), true); + } + + [Theory, BitAutoData] + public async Task RemoveUser_WithEventSystemUser_Success( + [OrganizationUser(type: OrganizationUserType.User)] OrganizationUser organizationUser, + EventSystemUser eventSystemUser, + SutProvider sutProvider) + { + var organizationUserRepository = sutProvider.GetDependency(); + + organizationUserRepository.GetByIdAsync(organizationUser.Id).Returns(organizationUser); + + await sutProvider.Sut.RemoveUserAsync(organizationUser.OrganizationId, organizationUser.Id, eventSystemUser); + + await sutProvider.GetDependency().Received(1).LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed, eventSystemUser); + } + + [Theory, BitAutoData] + public async Task RemoveUser_ByUserId_Success( + [OrganizationUser(type: OrganizationUserType.User)] OrganizationUser organizationUser, + SutProvider sutProvider) + { + var organizationUserRepository = sutProvider.GetDependency(); + + organizationUserRepository + .GetByOrganizationAsync(organizationUser.OrganizationId, organizationUser.UserId!.Value) + .Returns(organizationUser); + + sutProvider.GetDependency() + .HasConfirmedOwnersExceptAsync( + organizationUser.OrganizationId, + Arg.Is>(i => i.Contains(organizationUser.Id)), + Arg.Any()) + .Returns(true); + + await sutProvider.Sut.RemoveUserAsync(organizationUser.OrganizationId, organizationUser.UserId.Value); + + await sutProvider.GetDependency().Received(1).LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed); + } + + [Theory, BitAutoData] + public async Task RemoveUser_ByUserId_NotFound_ThrowsException(SutProvider sutProvider, + Guid organizationId, Guid userId) + { + await Assert.ThrowsAsync(async () => await sutProvider.Sut.RemoveUserAsync(organizationId, userId)); + } + + [Theory, BitAutoData] + public async Task RemoveUser_ByUserId_InvalidUser_ThrowsException(OrganizationUser organizationUser, + SutProvider sutProvider) + { + var organizationUserRepository = sutProvider.GetDependency(); + + organizationUserRepository.GetByOrganizationAsync(organizationUser.OrganizationId, organizationUser.UserId!.Value).Returns(organizationUser); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.RemoveUserAsync(Guid.NewGuid(), organizationUser.UserId.Value)); + Assert.Contains("User not found.", exception.Message); + } + + [Theory, BitAutoData] + public async Task RemoveUser_ByUserId_RemovingLastOwner_ThrowsException( + [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser organizationUser, + SutProvider sutProvider) + { + var organizationUserRepository = sutProvider.GetDependency(); + var hasConfirmedOwnersExceptQuery = sutProvider.GetDependency(); + + organizationUserRepository.GetByOrganizationAsync(organizationUser.OrganizationId, organizationUser.UserId!.Value).Returns(organizationUser); + hasConfirmedOwnersExceptQuery + .HasConfirmedOwnersExceptAsync( + organizationUser.OrganizationId, + Arg.Is>(i => i.Contains(organizationUser.Id)), + Arg.Any()) + .Returns(false); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.RemoveUserAsync(organizationUser.OrganizationId, organizationUser.UserId.Value)); + Assert.Contains("Organization must have at least one confirmed owner.", exception.Message); + hasConfirmedOwnersExceptQuery + .Received(1) + .HasConfirmedOwnersExceptAsync( + organizationUser.OrganizationId, + Arg.Is>(i => i.Contains(organizationUser.Id)), + Arg.Any()); + } + + [Theory, BitAutoData] + public async Task RemoveUsers_FilterInvalid_ThrowsException(OrganizationUser organizationUser, OrganizationUser deletingUser, + SutProvider sutProvider) + { + var organizationUserRepository = sutProvider.GetDependency(); + var organizationUsers = new[] { organizationUser }; + var organizationUserIds = organizationUsers.Select(u => u.Id); + organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(organizationUsers); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.RemoveUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId)); + Assert.Contains("Users invalid.", exception.Message); + } + + [Theory, BitAutoData] + public async Task RemoveUsers_RemoveYourself_ThrowsException( + OrganizationUser deletingUser, + SutProvider sutProvider) + { + var organizationUserRepository = sutProvider.GetDependency(); + var organizationUsers = new[] { deletingUser }; + var organizationUserIds = organizationUsers.Select(u => u.Id); + organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(organizationUsers); + sutProvider.GetDependency() + .HasConfirmedOwnersExceptAsync(deletingUser.OrganizationId, Arg.Any>()) + .Returns(true); + + var result = await sutProvider.Sut.RemoveUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId); + Assert.Contains("You cannot remove yourself.", result[0].Item2); + } + + [Theory, BitAutoData] + public async Task RemoveUsers_NonOwnerRemoveOwner_ThrowsException( + [OrganizationUser(type: OrganizationUserType.Admin)] OrganizationUser deletingUser, + [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser orgUser1, + [OrganizationUser(OrganizationUserStatusType.Confirmed)] OrganizationUser orgUser2, + SutProvider sutProvider) + { + var organizationUserRepository = sutProvider.GetDependency(); + + orgUser1.OrganizationId = orgUser2.OrganizationId = deletingUser.OrganizationId; + var organizationUsers = new[] { orgUser1 }; + var organizationUserIds = organizationUsers.Select(u => u.Id); + organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(organizationUsers); + sutProvider.GetDependency() + .HasConfirmedOwnersExceptAsync(deletingUser.OrganizationId, Arg.Any>()) + .Returns(true); + + var result = await sutProvider.Sut.RemoveUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId); + Assert.Contains("Only owners can delete other owners.", result[0].Item2); + } + + [Theory, BitAutoData] + public async Task RemoveUsers_LastOwner_ThrowsException( + [OrganizationUser(status: OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser orgUser, + SutProvider sutProvider) + { + var organizationUserRepository = sutProvider.GetDependency(); + + var organizationUsers = new[] { orgUser }; + var organizationUserIds = organizationUsers.Select(u => u.Id); + organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(organizationUsers); + organizationUserRepository.GetManyByOrganizationAsync(orgUser.OrganizationId, OrganizationUserType.Owner).Returns(organizationUsers); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.RemoveUsersAsync(orgUser.OrganizationId, organizationUserIds, null)); + Assert.Contains("Organization must have at least one confirmed owner.", exception.Message); + } + + [Theory, BitAutoData] + public async Task RemoveUsers_Success( + [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser deletingUser, + [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser orgUser1, OrganizationUser orgUser2, + SutProvider sutProvider) + { + var organizationUserRepository = sutProvider.GetDependency(); + var currentContext = sutProvider.GetDependency(); + + orgUser1.OrganizationId = orgUser2.OrganizationId = deletingUser.OrganizationId; + var organizationUsers = new[] { orgUser1, orgUser2 }; + var organizationUserIds = organizationUsers.Select(u => u.Id); + organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(organizationUsers); + organizationUserRepository.GetByIdAsync(deletingUser.Id).Returns(deletingUser); + sutProvider.GetDependency() + .HasConfirmedOwnersExceptAsync(deletingUser.OrganizationId, Arg.Any>()) + .Returns(true); + currentContext.OrganizationOwner(deletingUser.OrganizationId).Returns(true); + + await sutProvider.Sut.RemoveUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId); } } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommandTests.cs index a7db63fb5..73bf00474 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommandTests.cs @@ -1,6 +1,7 @@ using System.Text.Json; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Entities; using Bit.Core.Enums; @@ -169,7 +170,7 @@ public class UpdateOrganizationUserCommandTests await organizationService.Received(1).ValidateOrganizationCustomPermissionsEnabledAsync( newUserData.OrganizationId, newUserData.Type); - await organizationService.Received(1).HasConfirmedOwnersExceptAsync( + await sutProvider.GetDependency().Received(1).HasConfirmedOwnersExceptAsync( newUserData.OrganizationId, Arg.Is>(i => i.Contains(newUserData.Id))); } @@ -187,7 +188,7 @@ public class UpdateOrganizationUserCommandTests newUser.UserId = oldUser.UserId; newUser.OrganizationId = oldUser.OrganizationId = organization.Id; organizationUserRepository.GetByIdAsync(oldUser.Id).Returns(oldUser); - organizationService + sutProvider.GetDependency() .HasConfirmedOwnersExceptAsync( oldUser.OrganizationId, Arg.Is>(i => i.Contains(oldUser.Id))) diff --git a/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs b/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs index 1193b2de8..147162c66 100644 --- a/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs +++ b/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs @@ -7,7 +7,6 @@ using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Enums; -using Bit.Core.Auth.Models; using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.Repositories; @@ -44,6 +43,8 @@ using Xunit; using Organization = Bit.Core.AdminConsole.Entities.Organization; using OrganizationUser = Bit.Core.Entities.OrganizationUser; +#nullable enable + namespace Bit.Core.Test.Services; [SutProviderCustomize] @@ -78,8 +79,9 @@ public class OrganizationServiceTests .Returns(existingUsers); organizationUserRepository.GetCountByOrganizationIdAsync(org.Id) .Returns(existingUsers.Count); - organizationUserRepository.GetManyByOrganizationAsync(org.Id, OrganizationUserType.Owner) - .Returns(existingUsers.Select(u => new OrganizationUser { Status = OrganizationUserStatusType.Confirmed, Type = OrganizationUserType.Owner, Id = u.Id }).ToList()); + sutProvider.GetDependency() + .HasConfirmedOwnersExceptAsync(org.Id, Arg.Any>()) + .Returns(true); sutProvider.GetDependency().ManageUsers(org.Id).Returns(true); // Mock tokenable factory to return a token that expires in 5 days @@ -148,8 +150,9 @@ public class OrganizationServiceTests var organizationUserRepository = sutProvider.GetDependency(); - organizationUserRepository.GetManyByOrganizationAsync(org.Id, OrganizationUserType.Owner) - .Returns(existingUsers.Select(u => new OrganizationUser { Status = OrganizationUserStatusType.Confirmed, Type = OrganizationUserType.Owner, Id = u.Id }).ToList()); + sutProvider.GetDependency() + .HasConfirmedOwnersExceptAsync(org.Id, Arg.Any>()) + .Returns(true); SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository); @@ -709,8 +712,9 @@ OrganizationUserInvite invite, SutProvider sutProvider) var currentContext = sutProvider.GetDependency(); organizationRepository.GetByIdAsync(organization.Id).Returns(organization); - organizationUserRepository.GetManyByOrganizationAsync(organization.Id, OrganizationUserType.Owner) - .Returns(new[] { invitor }); + sutProvider.GetDependency() + .HasConfirmedOwnersExceptAsync(organization.Id, Arg.Any>()) + .Returns(true); SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository); @@ -738,8 +742,9 @@ OrganizationUserInvite invite, SutProvider sutProvider) var currentContext = sutProvider.GetDependency(); organizationRepository.GetByIdAsync(organization.Id).Returns(organization); - organizationUserRepository.GetManyByOrganizationAsync(organization.Id, OrganizationUserType.Owner) - .Returns(new[] { invitor }); + sutProvider.GetDependency() + .HasConfirmedOwnersExceptAsync(organization.Id, Arg.Any>()) + .Returns(true); SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository); @@ -818,8 +823,9 @@ OrganizationUserInvite invite, SutProvider sutProvider) var currentContext = sutProvider.GetDependency(); organizationRepository.GetByIdAsync(organization.Id).Returns(organization); - organizationUserRepository.GetManyByOrganizationAsync(organization.Id, OrganizationUserType.Owner) - .Returns(new[] { invitor }); + sutProvider.GetDependency() + .HasConfirmedOwnersExceptAsync(organization.Id, Arg.Any>()) + .Returns(true); SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository); @@ -836,7 +842,6 @@ OrganizationUserInvite invite, SutProvider sutProvider) ), OrganizationCustomize, BitAutoData] public async Task InviteUser_Passes(Organization organization, OrganizationUserInvite invite, string externalId, OrganizationUser invitor, - [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner, SutProvider sutProvider) { // This method is only used to invite 1 user at a time @@ -852,8 +857,6 @@ OrganizationUserInvite invite, SutProvider sutProvider) var organizationUserRepository = sutProvider.GetDependency(); organizationRepository.GetByIdAsync(organization.Id).Returns(organization); - organizationUserRepository.GetManyByOrganizationAsync(organization.Id, OrganizationUserType.Owner) - .Returns(new[] { owner }); // Mock tokenable factory to return a token that expires in 5 days sutProvider.GetDependency() @@ -865,6 +868,10 @@ OrganizationUserInvite invite, SutProvider sutProvider) } ); + sutProvider.GetDependency() + .HasConfirmedOwnersExceptAsync(organization.Id, Arg.Any>()) + .Returns(true); + SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository); SetupOrgUserRepositoryCreateAsyncMock(organizationUserRepository); @@ -906,7 +913,6 @@ OrganizationUserInvite invite, SutProvider sutProvider) ), OrganizationCustomize, BitAutoData] public async Task InviteUser_UserAlreadyInvited_Throws(Organization organization, OrganizationUserInvite invite, string externalId, OrganizationUser invitor, - [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner, SutProvider sutProvider) { // This method is only used to invite 1 user at a time @@ -927,8 +933,6 @@ OrganizationUserInvite invite, SutProvider sutProvider) var organizationUserRepository = sutProvider.GetDependency(); organizationRepository.GetByIdAsync(organization.Id).Returns(organization); - organizationUserRepository.GetManyByOrganizationAsync(organization.Id, OrganizationUserType.Owner) - .Returns(new[] { owner }); // Mock tokenable factory to return a token that expires in 5 days sutProvider.GetDependency() @@ -940,6 +944,10 @@ OrganizationUserInvite invite, SutProvider sutProvider) } ); + sutProvider.GetDependency() + .HasConfirmedOwnersExceptAsync(organization.Id, Arg.Any>()) + .Returns(true); + SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository); SetupOrgUserRepositoryCreateAsyncMock(organizationUserRepository); @@ -988,7 +996,6 @@ OrganizationUserInvite invite, SutProvider sutProvider) ), OrganizationCustomize, BitAutoData] public async Task InviteUsers_Passes(Organization organization, IEnumerable<(OrganizationUserInvite invite, string externalId)> invites, OrganizationUser invitor, - [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner, SutProvider sutProvider) { // Setup FakeDataProtectorTokenFactory for creating new tokens - this must come first in order to avoid resetting mocks @@ -1001,8 +1008,6 @@ OrganizationUserInvite invite, SutProvider sutProvider) var organizationUserRepository = sutProvider.GetDependency(); organizationRepository.GetByIdAsync(organization.Id).Returns(organization); - organizationUserRepository.GetManyByOrganizationAsync(organization.Id, OrganizationUserType.Owner) - .Returns(new[] { owner }); // Mock tokenable factory to return a token that expires in 5 days sutProvider.GetDependency() @@ -1014,6 +1019,10 @@ OrganizationUserInvite invite, SutProvider sutProvider) } ); + sutProvider.GetDependency() + .HasConfirmedOwnersExceptAsync(organization.Id, Arg.Any>()) + .Returns(true); + SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository); SetupOrgUserRepositoryCreateAsyncMock(organizationUserRepository); @@ -1035,7 +1044,6 @@ OrganizationUserInvite invite, SutProvider sutProvider) ), OrganizationCustomize, BitAutoData] public async Task InviteUsers_WithEventSystemUser_Passes(Organization organization, EventSystemUser eventSystemUser, IEnumerable<(OrganizationUserInvite invite, string externalId)> invites, OrganizationUser invitor, - [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner, SutProvider sutProvider) { // Setup FakeDataProtectorTokenFactory for creating new tokens - this must come first in order to avoid resetting mocks @@ -1053,8 +1061,9 @@ OrganizationUserInvite invite, SutProvider sutProvider) var currentContext = sutProvider.GetDependency(); organizationRepository.GetByIdAsync(organization.Id).Returns(organization); - organizationUserRepository.GetManyByOrganizationAsync(organization.Id, OrganizationUserType.Owner) - .Returns(new[] { owner }); + sutProvider.GetDependency() + .HasConfirmedOwnersExceptAsync(organization.Id, Arg.Any>()) + .Returns(true); SetupOrgUserRepositoryCreateAsyncMock(organizationUserRepository); SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository); @@ -1182,208 +1191,17 @@ OrganizationUserInvite invite, SutProvider sutProvider) sutProvider.GetDependency().ManageUsers(organization.Id).Returns(true); } - [Theory, BitAutoData] - public async Task RemoveUser_InvalidUser(OrganizationUser organizationUser, OrganizationUser deletingUser, - SutProvider sutProvider) - { - var organizationUserRepository = sutProvider.GetDependency(); - - organizationUserRepository.GetByIdAsync(organizationUser.Id).Returns(organizationUser); - - var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.RemoveUserAsync(Guid.NewGuid(), organizationUser.Id, deletingUser.UserId)); - Assert.Contains("User not valid.", exception.Message); - } - - [Theory, BitAutoData] - public async Task RemoveUser_RemoveYourself(OrganizationUser deletingUser, SutProvider sutProvider) - { - var organizationUserRepository = sutProvider.GetDependency(); - - organizationUserRepository.GetByIdAsync(deletingUser.Id).Returns(deletingUser); - - var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.RemoveUserAsync(deletingUser.OrganizationId, deletingUser.Id, deletingUser.UserId)); - Assert.Contains("You cannot remove yourself.", exception.Message); - } - - [Theory, BitAutoData] - public async Task RemoveUser_NonOwnerRemoveOwner( - [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser organizationUser, - [OrganizationUser(type: OrganizationUserType.Admin)] OrganizationUser deletingUser, - SutProvider sutProvider) - { - var organizationUserRepository = sutProvider.GetDependency(); - var currentContext = sutProvider.GetDependency(); - - organizationUser.OrganizationId = deletingUser.OrganizationId; - organizationUserRepository.GetByIdAsync(organizationUser.Id).Returns(organizationUser); - currentContext.OrganizationAdmin(deletingUser.OrganizationId).Returns(true); - - var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.RemoveUserAsync(deletingUser.OrganizationId, organizationUser.Id, deletingUser.UserId)); - Assert.Contains("Only owners can delete other owners.", exception.Message); - } - - [Theory, BitAutoData] - public async Task RemoveUser_LastOwner( - [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser organizationUser, - OrganizationUser deletingUser, - SutProvider sutProvider) - { - var organizationUserRepository = sutProvider.GetDependency(); - - organizationUser.OrganizationId = deletingUser.OrganizationId; - organizationUserRepository.GetByIdAsync(organizationUser.Id).Returns(organizationUser); - organizationUserRepository.GetManyByOrganizationAsync(deletingUser.OrganizationId, OrganizationUserType.Owner) - .Returns(new[] { organizationUser }); - - var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.RemoveUserAsync(deletingUser.OrganizationId, organizationUser.Id, null)); - Assert.Contains("Organization must have at least one confirmed owner.", exception.Message); - } - - [Theory, BitAutoData] - public async Task RemoveUser_Success( - OrganizationUser organizationUser, - [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser deletingUser, - SutProvider sutProvider) - { - var organizationUserRepository = sutProvider.GetDependency(); - var currentContext = sutProvider.GetDependency(); - - organizationUser.OrganizationId = deletingUser.OrganizationId; - organizationUserRepository.GetByIdAsync(organizationUser.Id).Returns(organizationUser); - organizationUserRepository.GetByIdAsync(deletingUser.Id).Returns(deletingUser); - organizationUserRepository.GetManyByOrganizationAsync(deletingUser.OrganizationId, OrganizationUserType.Owner) - .Returns(new[] { deletingUser, organizationUser }); - currentContext.OrganizationOwner(deletingUser.OrganizationId).Returns(true); - - await sutProvider.Sut.RemoveUserAsync(deletingUser.OrganizationId, organizationUser.Id, deletingUser.UserId); - - await sutProvider.GetDependency().Received(1).LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed); - } - - [Theory, BitAutoData] - public async Task RemoveUser_WithEventSystemUser_Success( - OrganizationUser organizationUser, - [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser deletingUser, EventSystemUser eventSystemUser, - SutProvider sutProvider) - { - var organizationUserRepository = sutProvider.GetDependency(); - var currentContext = sutProvider.GetDependency(); - - organizationUser.OrganizationId = deletingUser.OrganizationId; - organizationUserRepository.GetByIdAsync(organizationUser.Id).Returns(organizationUser); - organizationUserRepository.GetByIdAsync(deletingUser.Id).Returns(deletingUser); - organizationUserRepository.GetManyByOrganizationAsync(deletingUser.OrganizationId, OrganizationUserType.Owner) - .Returns(new[] { deletingUser, organizationUser }); - currentContext.OrganizationOwner(deletingUser.OrganizationId).Returns(true); - - await sutProvider.Sut.RemoveUserAsync(deletingUser.OrganizationId, organizationUser.Id, eventSystemUser); - - await sutProvider.GetDependency().Received(1).LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed, eventSystemUser); - } - - [Theory, BitAutoData] - public async Task RemoveUsers_FilterInvalid(OrganizationUser organizationUser, OrganizationUser deletingUser, - SutProvider sutProvider) - { - var organizationUserRepository = sutProvider.GetDependency(); - var organizationUsers = new[] { organizationUser }; - var organizationUserIds = organizationUsers.Select(u => u.Id); - organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(organizationUsers); - - var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.RemoveUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId)); - Assert.Contains("Users invalid.", exception.Message); - } - - [Theory, BitAutoData] - public async Task RemoveUsers_RemoveYourself( - [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser orgUser, - OrganizationUser deletingUser, - SutProvider sutProvider) - { - var organizationUserRepository = sutProvider.GetDependency(); - var organizationUsers = new[] { deletingUser }; - var organizationUserIds = organizationUsers.Select(u => u.Id); - organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(organizationUsers); - organizationUserRepository.GetManyByOrganizationAsync(default, default).ReturnsForAnyArgs(new[] { orgUser }); - - var result = await sutProvider.Sut.RemoveUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId); - Assert.Contains("You cannot remove yourself.", result[0].Item2); - } - - [Theory, BitAutoData] - public async Task RemoveUsers_NonOwnerRemoveOwner( - [OrganizationUser(type: OrganizationUserType.Admin)] OrganizationUser deletingUser, - [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser orgUser1, - [OrganizationUser(OrganizationUserStatusType.Confirmed)] OrganizationUser orgUser2, - SutProvider sutProvider) - { - var organizationUserRepository = sutProvider.GetDependency(); - - orgUser1.OrganizationId = orgUser2.OrganizationId = deletingUser.OrganizationId; - var organizationUsers = new[] { orgUser1 }; - var organizationUserIds = organizationUsers.Select(u => u.Id); - organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(organizationUsers); - organizationUserRepository.GetManyByOrganizationAsync(default, default).ReturnsForAnyArgs(new[] { orgUser2 }); - - var result = await sutProvider.Sut.RemoveUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId); - Assert.Contains("Only owners can delete other owners.", result[0].Item2); - } - - [Theory, BitAutoData] - public async Task RemoveUsers_LastOwner( - [OrganizationUser(status: OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser orgUser, - SutProvider sutProvider) - { - var organizationUserRepository = sutProvider.GetDependency(); - - var organizationUsers = new[] { orgUser }; - var organizationUserIds = organizationUsers.Select(u => u.Id); - organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(organizationUsers); - organizationUserRepository.GetManyByOrganizationAsync(orgUser.OrganizationId, OrganizationUserType.Owner).Returns(organizationUsers); - - var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.RemoveUsersAsync(orgUser.OrganizationId, organizationUserIds, null)); - Assert.Contains("Organization must have at least one confirmed owner.", exception.Message); - } - - [Theory, BitAutoData] - public async Task RemoveUsers_Success( - [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser deletingUser, - [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser orgUser1, OrganizationUser orgUser2, - SutProvider sutProvider) - { - var organizationUserRepository = sutProvider.GetDependency(); - var currentContext = sutProvider.GetDependency(); - - orgUser1.OrganizationId = orgUser2.OrganizationId = deletingUser.OrganizationId; - var organizationUsers = new[] { orgUser1, orgUser2 }; - var organizationUserIds = organizationUsers.Select(u => u.Id); - organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(organizationUsers); - organizationUserRepository.GetByIdAsync(deletingUser.Id).Returns(deletingUser); - organizationUserRepository.GetManyByOrganizationAsync(deletingUser.OrganizationId, OrganizationUserType.Owner) - .Returns(new[] { deletingUser, orgUser1 }); - currentContext.OrganizationOwner(deletingUser.OrganizationId).Returns(true); - - await sutProvider.Sut.RemoveUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId); - } - [Theory, BitAutoData] public async Task ConfirmUser_InvalidStatus(OrganizationUser confirmingUser, [OrganizationUser(OrganizationUserStatusType.Invited)] OrganizationUser orgUser, string key, SutProvider sutProvider) { var organizationUserRepository = sutProvider.GetDependency(); - var userService = Substitute.For(); organizationUserRepository.GetByIdAsync(orgUser.Id).Returns(orgUser); var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, userService)); + () => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id)); Assert.Contains("User not valid.", exception.Message); } @@ -1393,12 +1211,11 @@ OrganizationUserInvite invite, SutProvider sutProvider) SutProvider sutProvider) { var organizationUserRepository = sutProvider.GetDependency(); - var userService = Substitute.For(); organizationUserRepository.GetByIdAsync(orgUser.Id).Returns(orgUser); var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.ConfirmUserAsync(confirmingUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, userService)); + () => sutProvider.Sut.ConfirmUserAsync(confirmingUser.OrganizationId, orgUser.Id, key, confirmingUser.Id)); Assert.Contains("User not valid.", exception.Message); } @@ -1411,7 +1228,6 @@ OrganizationUserInvite invite, SutProvider sutProvider) { var organizationUserRepository = sutProvider.GetDependency(); var organizationRepository = sutProvider.GetDependency(); - var userService = Substitute.For(); var userRepository = sutProvider.GetDependency(); org.PlanType = PlanType.Free; @@ -1424,7 +1240,7 @@ OrganizationUserInvite invite, SutProvider sutProvider) userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user }); var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, userService)); + () => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id)); Assert.Contains("User can only be an admin of one free organization.", exception.Message); } @@ -1465,7 +1281,6 @@ OrganizationUserInvite invite, SutProvider sutProvider) { var organizationUserRepository = sutProvider.GetDependency(); var organizationRepository = sutProvider.GetDependency(); - var userService = Substitute.For(); var userRepository = sutProvider.GetDependency(); org.PlanType = planType; @@ -1478,7 +1293,7 @@ OrganizationUserInvite invite, SutProvider sutProvider) organizationRepository.GetByIdAsync(org.Id).Returns(org); userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user }); - await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, userService); + await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id); await sutProvider.GetDependency().Received(1).LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed); await sutProvider.GetDependency().Received(1).SendOrganizationConfirmedEmailAsync(org.DisplayName(), user.Email); @@ -1496,7 +1311,6 @@ OrganizationUserInvite invite, SutProvider sutProvider) var organizationRepository = sutProvider.GetDependency(); var userRepository = sutProvider.GetDependency(); var policyService = sutProvider.GetDependency(); - var userService = Substitute.For(); org.PlanType = PlanType.EnterpriseAnnually; orgUser.Status = OrganizationUserStatusType.Accepted; @@ -1510,7 +1324,7 @@ OrganizationUserInvite invite, SutProvider sutProvider) policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg).Returns(new[] { singleOrgPolicy }); var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, userService)); + () => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id)); Assert.Contains("Cannot confirm this member to the organization until they leave or remove all other organizations.", exception.Message); } @@ -1524,7 +1338,6 @@ OrganizationUserInvite invite, SutProvider sutProvider) var organizationRepository = sutProvider.GetDependency(); var userRepository = sutProvider.GetDependency(); var policyService = sutProvider.GetDependency(); - var userService = Substitute.For(); org.PlanType = PlanType.EnterpriseAnnually; orgUser.Status = OrganizationUserStatusType.Accepted; @@ -1538,7 +1351,7 @@ OrganizationUserInvite invite, SutProvider sutProvider) policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg).Returns(new[] { singleOrgPolicy }); var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, userService)); + () => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id)); Assert.Contains("Cannot confirm this member to the organization because they are in another organization which forbids it.", exception.Message); } @@ -1554,7 +1367,6 @@ OrganizationUserInvite invite, SutProvider sutProvider) var organizationUserRepository = sutProvider.GetDependency(); var organizationRepository = sutProvider.GetDependency(); var userRepository = sutProvider.GetDependency(); - var userService = Substitute.For(); org.PlanType = PlanType.EnterpriseAnnually; orgUser.Type = userType; @@ -1567,7 +1379,7 @@ OrganizationUserInvite invite, SutProvider sutProvider) organizationRepository.GetByIdAsync(org.Id).Returns(org); userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user }); - await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, userService); + await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id); await sutProvider.GetDependency().Received(1).LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed); await sutProvider.GetDependency().Received(1).SendOrganizationConfirmedEmailAsync(org.DisplayName(), user.Email, true); @@ -1585,7 +1397,7 @@ OrganizationUserInvite invite, SutProvider sutProvider) var organizationRepository = sutProvider.GetDependency(); var userRepository = sutProvider.GetDependency(); var policyService = sutProvider.GetDependency(); - var userService = Substitute.For(); + var twoFactorIsEnabledQuery = sutProvider.GetDependency(); org.PlanType = PlanType.EnterpriseAnnually; orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id; @@ -1596,9 +1408,11 @@ OrganizationUserInvite invite, SutProvider sutProvider) userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user }); twoFactorPolicy.OrganizationId = org.Id; policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication).Returns(new[] { twoFactorPolicy }); + twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(Arg.Is>(ids => ids.Contains(user.Id))) + .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (user.Id, false) }); var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, userService)); + () => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id)); Assert.Contains("User does not have two-step login enabled.", exception.Message); } @@ -1612,7 +1426,7 @@ OrganizationUserInvite invite, SutProvider sutProvider) var organizationRepository = sutProvider.GetDependency(); var userRepository = sutProvider.GetDependency(); var policyService = sutProvider.GetDependency(); - var userService = Substitute.For(); + var twoFactorIsEnabledQuery = sutProvider.GetDependency(); org.PlanType = PlanType.EnterpriseAnnually; orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id; @@ -1622,71 +1436,10 @@ OrganizationUserInvite invite, SutProvider sutProvider) userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user }); twoFactorPolicy.OrganizationId = org.Id; policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication).Returns(new[] { twoFactorPolicy }); - userService.TwoFactorIsEnabledAsync(user).Returns(true); - - await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, userService); - } - - [Theory, BitAutoData] - public async Task ConfirmUser_vNext_TwoFactorPolicy_NotEnabled_Throws(Organization org, OrganizationUser confirmingUser, - [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, UserWithCalculatedPremium user, - OrganizationUser orgUserAnotherOrg, - [OrganizationUserPolicyDetails(PolicyType.TwoFactorAuthentication)] OrganizationUserPolicyDetails twoFactorPolicy, - string key, SutProvider sutProvider) - { - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization).Returns(true); - - var organizationUserRepository = sutProvider.GetDependency(); - var organizationRepository = sutProvider.GetDependency(); - var userRepository = sutProvider.GetDependency(); - var policyService = sutProvider.GetDependency(); - var userService = Substitute.For(); - var twoFactorIsEnabledQuery = sutProvider.GetDependency(); - - org.PlanType = PlanType.EnterpriseAnnually; - orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id; - orgUser.UserId = orgUserAnotherOrg.UserId = user.Id; - organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser }); - organizationUserRepository.GetManyByManyUsersAsync(default).ReturnsForAnyArgs(new[] { orgUserAnotherOrg }); - organizationRepository.GetByIdAsync(org.Id).Returns(org); - userRepository.GetManyWithCalculatedPremiumAsync(default).ReturnsForAnyArgs(new[] { user }); - twoFactorPolicy.OrganizationId = org.Id; - policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication).Returns(new[] { twoFactorPolicy }); - twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(Arg.Is>(ids => ids.Contains(user.Id))) - .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (user.Id, false) }); - - var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, userService)); - Assert.Contains("User does not have two-step login enabled.", exception.Message); - } - - [Theory, BitAutoData] - public async Task ConfirmUser_vNext_TwoFactorPolicy_Enabled_Success(Organization org, OrganizationUser confirmingUser, - [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, UserWithCalculatedPremium user, - [OrganizationUserPolicyDetails(PolicyType.TwoFactorAuthentication)] OrganizationUserPolicyDetails twoFactorPolicy, - string key, SutProvider sutProvider) - { - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization).Returns(true); - - var organizationUserRepository = sutProvider.GetDependency(); - var organizationRepository = sutProvider.GetDependency(); - var userRepository = sutProvider.GetDependency(); - var policyService = sutProvider.GetDependency(); - var userService = Substitute.For(); - var twoFactorIsEnabledQuery = sutProvider.GetDependency(); - - org.PlanType = PlanType.EnterpriseAnnually; - orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id; - orgUser.UserId = user.Id; - organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser }); - organizationRepository.GetByIdAsync(org.Id).Returns(org); - userRepository.GetManyWithCalculatedPremiumAsync(default).ReturnsForAnyArgs(new[] { user }); - twoFactorPolicy.OrganizationId = org.Id; - policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication).Returns(new[] { twoFactorPolicy }); twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(Arg.Is>(ids => ids.Contains(user.Id))) .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (user.Id, true) }); - await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, userService); + await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id); } [Theory, BitAutoData] @@ -1704,52 +1457,6 @@ OrganizationUserInvite invite, SutProvider sutProvider) var organizationRepository = sutProvider.GetDependency(); var userRepository = sutProvider.GetDependency(); var policyService = sutProvider.GetDependency(); - var userService = Substitute.For(); - - org.PlanType = PlanType.EnterpriseAnnually; - orgUser1.OrganizationId = orgUser2.OrganizationId = orgUser3.OrganizationId = confirmingUser.OrganizationId = org.Id; - orgUser1.UserId = user1.Id; - orgUser2.UserId = user2.Id; - orgUser3.UserId = user3.Id; - anotherOrgUser.UserId = user3.Id; - var orgUsers = new[] { orgUser1, orgUser2, orgUser3 }; - organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(orgUsers); - organizationRepository.GetByIdAsync(org.Id).Returns(org); - userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user1, user2, user3 }); - twoFactorPolicy.OrganizationId = org.Id; - policyService.GetPoliciesApplicableToUserAsync(Arg.Any(), PolicyType.TwoFactorAuthentication).Returns(new[] { twoFactorPolicy }); - userService.TwoFactorIsEnabledAsync(user1).Returns(true); - userService.TwoFactorIsEnabledAsync(user2).Returns(false); - userService.TwoFactorIsEnabledAsync(user3).Returns(true); - singleOrgPolicy.OrganizationId = org.Id; - policyService.GetPoliciesApplicableToUserAsync(user3.Id, PolicyType.SingleOrg) - .Returns(new[] { singleOrgPolicy }); - organizationUserRepository.GetManyByManyUsersAsync(default) - .ReturnsForAnyArgs(new[] { orgUser1, orgUser2, orgUser3, anotherOrgUser }); - - var keys = orgUsers.ToDictionary(ou => ou.Id, _ => key); - var result = await sutProvider.Sut.ConfirmUsersAsync(confirmingUser.OrganizationId, keys, confirmingUser.Id, userService); - Assert.Contains("", result[0].Item2); - Assert.Contains("User does not have two-step login enabled.", result[1].Item2); - Assert.Contains("Cannot confirm this member to the organization until they leave or remove all other organizations.", result[2].Item2); - } - - [Theory, BitAutoData] - public async Task ConfirmUsers_vNext_Success(Organization org, - OrganizationUser confirmingUser, - [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser1, - [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser2, - [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser3, - OrganizationUser anotherOrgUser, UserWithCalculatedPremium user1, UserWithCalculatedPremium user2, UserWithCalculatedPremium user3, - [OrganizationUserPolicyDetails(PolicyType.TwoFactorAuthentication)] OrganizationUserPolicyDetails twoFactorPolicy, - [OrganizationUserPolicyDetails(PolicyType.SingleOrg)] OrganizationUserPolicyDetails singleOrgPolicy, - string key, SutProvider sutProvider) - { - var organizationUserRepository = sutProvider.GetDependency(); - var organizationRepository = sutProvider.GetDependency(); - var userRepository = sutProvider.GetDependency(); - var policyService = sutProvider.GetDependency(); - var userService = Substitute.For(); var twoFactorIsEnabledQuery = sutProvider.GetDependency(); org.PlanType = PlanType.EnterpriseAnnually; @@ -1761,7 +1468,7 @@ OrganizationUserInvite invite, SutProvider sutProvider) var orgUsers = new[] { orgUser1, orgUser2, orgUser3 }; organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(orgUsers); organizationRepository.GetByIdAsync(org.Id).Returns(org); - userRepository.GetManyWithCalculatedPremiumAsync(default).ReturnsForAnyArgs(new[] { user1, user2, user3 }); + userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user1, user2, user3 }); twoFactorPolicy.OrganizationId = org.Id; policyService.GetPoliciesApplicableToUserAsync(Arg.Any(), PolicyType.TwoFactorAuthentication).Returns(new[] { twoFactorPolicy }); twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(Arg.Is>(ids => ids.Contains(user1.Id) && ids.Contains(user2.Id) && ids.Contains(user3.Id))) @@ -1778,7 +1485,7 @@ OrganizationUserInvite invite, SutProvider sutProvider) .ReturnsForAnyArgs(new[] { orgUser1, orgUser2, orgUser3, anotherOrgUser }); var keys = orgUsers.ToDictionary(ou => ou.Id, _ => key); - var result = await sutProvider.Sut.ConfirmUsersAsync_vNext(confirmingUser.OrganizationId, keys, confirmingUser.Id); + var result = await sutProvider.Sut.ConfirmUsersAsync(confirmingUser.OrganizationId, keys, confirmingUser.Id); Assert.Contains("", result[0].Item2); Assert.Contains("User does not have two-step login enabled.", result[1].Item2); Assert.Contains("Cannot confirm this member to the organization until they leave or remove all other organizations.", result[2].Item2); @@ -1971,17 +1678,24 @@ OrganizationUserInvite invite, SutProvider sutProvider) await applicationCacheService.DidNotReceiveWithAnyArgs().DeleteOrganizationAbilityAsync(default); } - private void RestoreRevokeUser_Setup(Organization organization, OrganizationUser restoringUser, - OrganizationUser organizationUser, SutProvider sutProvider, - OrganizationUserType restoringUserType = OrganizationUserType.Owner) + private void RestoreRevokeUser_Setup( + Organization organization, + OrganizationUser? requestingOrganizationUser, + OrganizationUser targetOrganizationUser, + SutProvider sutProvider) { + if (requestingOrganizationUser != null) + { + requestingOrganizationUser.OrganizationId = organization.Id; + } + targetOrganizationUser.OrganizationId = organization.Id; + sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); - sutProvider.GetDependency().GetByIdAsync(organizationUser.OrganizationId).Returns(organization); - sutProvider.GetDependency().OrganizationOwner(organization.Id).Returns(true); - sutProvider.GetDependency().ManageUsers(organization.Id).Returns(true); - var organizationUserRepository = sutProvider.GetDependency(); - organizationUserRepository.GetManyByOrganizationAsync(organizationUser.OrganizationId, restoringUserType) - .Returns(new[] { restoringUser }); + sutProvider.GetDependency().OrganizationOwner(organization.Id).Returns(requestingOrganizationUser != null && requestingOrganizationUser.Type is OrganizationUserType.Owner); + sutProvider.GetDependency().ManageUsers(organization.Id).Returns(requestingOrganizationUser != null && (requestingOrganizationUser.Type is OrganizationUserType.Owner or OrganizationUserType.Admin)); + sutProvider.GetDependency() + .HasConfirmedOwnersExceptAsync(organization.Id, Arg.Any>()) + .Returns(true); } [Theory, BitAutoData] @@ -2000,10 +1714,9 @@ OrganizationUserInvite invite, SutProvider sutProvider) } [Theory, BitAutoData] - public async Task RevokeUser_WithEventSystemUser_Success(Organization organization, [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner, - [OrganizationUser] OrganizationUser organizationUser, EventSystemUser eventSystemUser, SutProvider sutProvider) + public async Task RevokeUser_WithEventSystemUser_Success(Organization organization, [OrganizationUser] OrganizationUser organizationUser, EventSystemUser eventSystemUser, SutProvider sutProvider) { - RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider); + RestoreRevokeUser_Setup(organization, null, organizationUser, sutProvider); var organizationUserRepository = sutProvider.GetDependency(); var eventService = sutProvider.GetDependency(); @@ -2019,11 +1732,10 @@ OrganizationUserInvite invite, SutProvider sutProvider) [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, SutProvider sutProvider) { RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider); - var userService = Substitute.For(); var organizationUserRepository = sutProvider.GetDependency(); var eventService = sutProvider.GetDependency(); - await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, userService); + await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id); await organizationUserRepository.Received().RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Invited); await eventService.Received() @@ -2031,15 +1743,13 @@ OrganizationUserInvite invite, SutProvider sutProvider) } [Theory, BitAutoData] - public async Task RestoreUser_WithEventSystemUser_Success(Organization organization, [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner, - [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, EventSystemUser eventSystemUser, SutProvider sutProvider) + public async Task RestoreUser_WithEventSystemUser_Success(Organization organization, [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, EventSystemUser eventSystemUser, SutProvider sutProvider) { - RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider); - var userService = Substitute.For(); + RestoreRevokeUser_Setup(organization, null, organizationUser, sutProvider); var organizationUserRepository = sutProvider.GetDependency(); var eventService = sutProvider.GetDependency(); - await sutProvider.Sut.RestoreUserAsync(organizationUser, eventSystemUser, userService); + await sutProvider.Sut.RestoreUserAsync(organizationUser, eventSystemUser); await organizationUserRepository.Received().RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Invited); await eventService.Received() @@ -2052,12 +1762,11 @@ OrganizationUserInvite invite, SutProvider sutProvider) { organizationUser.UserId = owner.Id; RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider); - var userService = Substitute.For(); var organizationUserRepository = sutProvider.GetDependency(); var eventService = sutProvider.GetDependency(); var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, userService)); + () => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id)); Assert.Contains("you cannot restore yourself", exception.Message.ToLowerInvariant()); @@ -2069,17 +1778,17 @@ OrganizationUserInvite invite, SutProvider sutProvider) [Theory] [BitAutoData(OrganizationUserType.Admin)] [BitAutoData(OrganizationUserType.Custom)] - public async Task RestoreUser_AdminRestoreOwner_Fails(OrganizationUserType restoringUserType, Organization organization, [OrganizationUser(OrganizationUserStatusType.Confirmed)] OrganizationUser restoringUser, + public async Task RestoreUser_AdminRestoreOwner_Fails(OrganizationUserType restoringUserType, + Organization organization, [OrganizationUser(OrganizationUserStatusType.Confirmed)] OrganizationUser restoringUser, [OrganizationUser(OrganizationUserStatusType.Revoked, OrganizationUserType.Owner)] OrganizationUser organizationUser, SutProvider sutProvider) { restoringUser.Type = restoringUserType; - RestoreRevokeUser_Setup(organization, restoringUser, organizationUser, sutProvider, OrganizationUserType.Admin); - var userService = Substitute.For(); + RestoreRevokeUser_Setup(organization, restoringUser, organizationUser, sutProvider); var organizationUserRepository = sutProvider.GetDependency(); var eventService = sutProvider.GetDependency(); var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.RestoreUserAsync(organizationUser, restoringUser.Id, userService)); + () => sutProvider.Sut.RestoreUserAsync(organizationUser, restoringUser.Id)); Assert.Contains("only owners can restore other owners", exception.Message.ToLowerInvariant()); @@ -2097,12 +1806,11 @@ OrganizationUserInvite invite, SutProvider sutProvider) { organizationUser.Status = userStatus; RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider); - var userService = Substitute.For(); var organizationUserRepository = sutProvider.GetDependency(); var eventService = sutProvider.GetDependency(); var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, userService)); + () => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id)); Assert.Contains("already active", exception.Message.ToLowerInvariant()); @@ -2111,37 +1819,6 @@ OrganizationUserInvite invite, SutProvider sutProvider) .LogOrganizationUserEventAsync(Arg.Any(), Arg.Any(), Arg.Any()); } - [Theory, BitAutoData] - public async Task RestoreUser_WithSingleOrgPolicyEnabled_Fails( - Organization organization, - [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner, - [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, - [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser secondOrganizationUser, - SutProvider sutProvider) - { - organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke - secondOrganizationUser.UserId = organizationUser.UserId; - RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider); - var userService = Substitute.For(); - var organizationUserRepository = sutProvider.GetDependency(); - var eventService = sutProvider.GetDependency(); - - organizationUserRepository.GetManyByUserAsync(organizationUser.UserId.Value).Returns(new[] { organizationUser, secondOrganizationUser }); - sutProvider.GetDependency() - .GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.SingleOrg, Arg.Any()) - .Returns(new[] { new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.SingleOrg } }); - - var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, userService)); - - Assert.Contains("you cannot restore this user until " + - "they leave or remove all other organizations.", exception.Message.ToLowerInvariant()); - - await organizationUserRepository.DidNotReceiveWithAnyArgs().RestoreAsync(Arg.Any(), Arg.Any()); - await eventService.DidNotReceiveWithAnyArgs() - .LogOrganizationUserEventAsync(Arg.Any(), Arg.Any(), Arg.Any()); - } - [Theory, BitAutoData] public async Task RestoreUser_WithOtherOrganizationSingleOrgPolicyEnabled_Fails( Organization organization, @@ -2151,7 +1828,6 @@ OrganizationUserInvite invite, SutProvider sutProvider) { organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider); - var userService = Substitute.For(); var organizationUserRepository = sutProvider.GetDependency(); var eventService = sutProvider.GetDependency(); @@ -2160,7 +1836,7 @@ OrganizationUserInvite invite, SutProvider sutProvider) .Returns(true); var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, userService)); + () => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id)); Assert.Contains("you cannot restore this user because they are a member of " + "another organization which forbids it", exception.Message.ToLowerInvariant()); @@ -2178,20 +1854,21 @@ OrganizationUserInvite invite, SutProvider sutProvider) SutProvider sutProvider) { organizationUser.Email = null; + + sutProvider.GetDependency() + .TwoFactorIsEnabledAsync(Arg.Is>(i => i.Contains(organizationUser.UserId.Value))) + .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (organizationUser.UserId.Value, false) }); + + RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider); + var organizationUserRepository = sutProvider.GetDependency(); + var eventService = sutProvider.GetDependency(); + sutProvider.GetDependency() .GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.TwoFactorAuthentication, Arg.Any()) .Returns(new[] { new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.TwoFactorAuthentication } }); - RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider); - var userService = Substitute.For(); - var organizationUserRepository = sutProvider.GetDependency(); - var eventService = sutProvider.GetDependency(); - - - userService.TwoFactorIsEnabledAsync(Arg.Any()).Returns(false); - var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, userService)); + () => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id)); Assert.Contains("you cannot restore this user until they enable " + "two-step login on their user account.", exception.Message.ToLowerInvariant()); @@ -2210,16 +1887,17 @@ OrganizationUserInvite invite, SutProvider sutProvider) { organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider); - var userService = Substitute.For(); var organizationUserRepository = sutProvider.GetDependency(); var eventService = sutProvider.GetDependency(); sutProvider.GetDependency() .GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.TwoFactorAuthentication, Arg.Any()) .Returns(new[] { new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.TwoFactorAuthentication } }); - userService.TwoFactorIsEnabledAsync(Arg.Any()).Returns(true); + sutProvider.GetDependency() + .TwoFactorIsEnabledAsync(Arg.Is>(i => i.Contains(organizationUser.UserId.Value))) + .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (organizationUser.UserId.Value, true) }); - await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, userService); + await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id); await organizationUserRepository.Received().RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Confirmed); await eventService.Received() @@ -2227,19 +1905,16 @@ OrganizationUserInvite invite, SutProvider sutProvider) } [Theory, BitAutoData] - public async Task RestoreUser_vNext_WithSingleOrgPolicyEnabled_Fails( + public async Task RestoreUser_WithSingleOrgPolicyEnabled_Fails( Organization organization, [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner, [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser secondOrganizationUser, SutProvider sutProvider) { - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization).Returns(true); - organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke secondOrganizationUser.UserId = organizationUser.UserId; RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider); - var userService = Substitute.For(); var organizationUserRepository = sutProvider.GetDependency(); var eventService = sutProvider.GetDependency(); @@ -2252,7 +1927,7 @@ OrganizationUserInvite invite, SutProvider sutProvider) }); var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, userService)); + () => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id)); Assert.Contains("you cannot restore this user until " + "they leave or remove all other organizations.", exception.Message.ToLowerInvariant()); @@ -2270,12 +1945,9 @@ OrganizationUserInvite invite, SutProvider sutProvider) [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser secondOrganizationUser, SutProvider sutProvider) { - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization).Returns(true); - organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke secondOrganizationUser.UserId = organizationUser.UserId; RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider); - var userService = Substitute.For(); var organizationUserRepository = sutProvider.GetDependency(); var eventService = sutProvider.GetDependency(); var twoFactorIsEnabledQuery = sutProvider.GetDependency(); @@ -2289,7 +1961,7 @@ OrganizationUserInvite invite, SutProvider sutProvider) .Returns(true); var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, userService)); + () => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id)); Assert.Contains("you cannot restore this user because they are a member of " + "another organization which forbids it", exception.Message.ToLowerInvariant()); @@ -2306,20 +1978,18 @@ OrganizationUserInvite invite, SutProvider sutProvider) [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, SutProvider sutProvider) { - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization).Returns(true); - organizationUser.Email = null; + + RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider); + var organizationUserRepository = sutProvider.GetDependency(); + var eventService = sutProvider.GetDependency(); + sutProvider.GetDependency() .GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.TwoFactorAuthentication, Arg.Any()) .Returns(new[] { new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.TwoFactorAuthentication } }); - RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider); - var userService = Substitute.For(); - var organizationUserRepository = sutProvider.GetDependency(); - var eventService = sutProvider.GetDependency(); - var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, userService)); + () => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id)); Assert.Contains("you cannot restore this user until they enable " + "two-step login on their user account.", exception.Message.ToLowerInvariant()); @@ -2336,11 +2006,8 @@ OrganizationUserInvite invite, SutProvider sutProvider) [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, SutProvider sutProvider) { - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization).Returns(true); - organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider); - var userService = Substitute.For(); var organizationUserRepository = sutProvider.GetDependency(); var eventService = sutProvider.GetDependency(); var twoFactorIsEnabledQuery = sutProvider.GetDependency(); @@ -2353,65 +2020,13 @@ OrganizationUserInvite invite, SutProvider sutProvider) .TwoFactorIsEnabledAsync(Arg.Is>(i => i.Contains(organizationUser.UserId.Value))) .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (organizationUser.UserId.Value, true) }); - await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, userService); + await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id); await organizationUserRepository.Received().RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Confirmed); await eventService.Received() .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored); } - [Theory, BitAutoData] - public async Task HasConfirmedOwnersExcept_WithConfirmedOwner_ReturnsTrue(Organization organization, [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner, SutProvider sutProvider) - { - sutProvider.GetDependency() - .GetManyByOrganizationAsync(organization.Id, OrganizationUserType.Owner) - .Returns(new List { owner }); - - var result = await sutProvider.Sut.HasConfirmedOwnersExceptAsync(organization.Id, new List(), true); - - Assert.True(result); - } - - [Theory, BitAutoData] - public async Task HasConfirmedOwnersExcept_ExcludingConfirmedOwner_ReturnsFalse(Organization organization, [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner, SutProvider sutProvider) - { - sutProvider.GetDependency() - .GetManyByOrganizationAsync(organization.Id, OrganizationUserType.Owner) - .Returns(new List { owner }); - - var result = await sutProvider.Sut.HasConfirmedOwnersExceptAsync(organization.Id, new List { owner.Id }, true); - - Assert.False(result); - } - - [Theory, BitAutoData] - public async Task HasConfirmedOwnersExcept_WithInvitedOwner_ReturnsFalse(Organization organization, [OrganizationUser(OrganizationUserStatusType.Invited, OrganizationUserType.Owner)] OrganizationUser owner, SutProvider sutProvider) - { - sutProvider.GetDependency() - .GetManyByOrganizationAsync(organization.Id, OrganizationUserType.Owner) - .Returns(new List { owner }); - - var result = await sutProvider.Sut.HasConfirmedOwnersExceptAsync(organization.Id, new List(), true); - - Assert.False(result); - } - - [Theory] - [BitAutoData(true)] - [BitAutoData(false)] - public async Task HasConfirmedOwnersExcept_WithConfirmedProviderUser_IncludeProviderTrue_ReturnsTrue(bool includeProvider, Organization organization, ProviderUser providerUser, SutProvider sutProvider) - { - providerUser.Status = ProviderUserStatusType.Confirmed; - - sutProvider.GetDependency() - .GetManyByOrganizationAsync(organization.Id, ProviderUserStatusType.Confirmed) - .Returns(new List { providerUser }); - - var result = await sutProvider.Sut.HasConfirmedOwnersExceptAsync(organization.Id, new List(), includeProvider); - - Assert.Equal(includeProvider, result); - } - [Theory] [BitAutoData(PlanType.TeamsAnnually)] [BitAutoData(PlanType.TeamsMonthly)] @@ -2635,61 +2250,6 @@ OrganizationUserInvite invite, SutProvider sutProvider) Assert.Contains("custom users can only grant the same custom permissions that they have.", exception.Message.ToLowerInvariant()); } - [Theory, BitAutoData] - public async Task HasConfirmedOwnersExceptAsync_WithConfirmedOwners_ReturnsTrue( - Guid organizationId, - IEnumerable organizationUsersId, - ICollection owners, - SutProvider sutProvider) - { - sutProvider.GetDependency() - .GetManyByOrganizationAsync(organizationId, OrganizationUserType.Owner) - .Returns(owners); - - var result = await sutProvider.Sut.HasConfirmedOwnersExceptAsync(organizationId, organizationUsersId); - - Assert.True(result); - } - - [Theory, BitAutoData] - public async Task HasConfirmedOwnersExceptAsync_WithConfirmedProviders_ReturnsTrue( - Guid organizationId, - IEnumerable organizationUsersId, - ICollection providerUsers, - SutProvider sutProvider) - { - sutProvider.GetDependency() - .GetManyByOrganizationAsync(organizationId, OrganizationUserType.Owner) - .Returns(new List()); - - sutProvider.GetDependency() - .GetManyByOrganizationAsync(organizationId, ProviderUserStatusType.Confirmed) - .Returns(providerUsers); - - var result = await sutProvider.Sut.HasConfirmedOwnersExceptAsync(organizationId, organizationUsersId); - - Assert.True(result); - } - - [Theory, BitAutoData] - public async Task HasConfirmedOwnersExceptAsync_WithNoConfirmedOwnersOrProviders_ReturnsFalse( - Guid organizationId, - IEnumerable organizationUsersId, - SutProvider sutProvider) - { - sutProvider.GetDependency() - .GetManyByOrganizationAsync(organizationId, OrganizationUserType.Owner) - .Returns(new List()); - - sutProvider.GetDependency() - .GetManyByOrganizationAsync(organizationId, ProviderUserStatusType.Confirmed) - .Returns(new List()); - - var result = await sutProvider.Sut.HasConfirmedOwnersExceptAsync(organizationId, organizationUsersId); - - Assert.False(result); - } - [Theory] [BitAutoData(OrganizationUserType.Owner)] [BitAutoData(OrganizationUserType.Admin)] diff --git a/test/Core.Test/AdminConsole/Services/PolicyServiceTests.cs b/test/Core.Test/AdminConsole/Services/PolicyServiceTests.cs index fd7597a74..fb08a32f2 100644 --- a/test/Core.Test/AdminConsole/Services/PolicyServiceTests.cs +++ b/test/Core.Test/AdminConsole/Services/PolicyServiceTests.cs @@ -1,12 +1,14 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services.Implementations; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.Repositories; +using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Data.Organizations.OrganizationUsers; @@ -32,7 +34,6 @@ public class PolicyServiceTests var badRequestException = await Assert.ThrowsAsync( () => sutProvider.Sut.SaveAsync(policy, - Substitute.For(), Substitute.For(), Guid.NewGuid())); @@ -60,7 +61,6 @@ public class PolicyServiceTests var badRequestException = await Assert.ThrowsAsync( () => sutProvider.Sut.SaveAsync(policy, - Substitute.For(), Substitute.For(), Guid.NewGuid())); @@ -93,7 +93,6 @@ public class PolicyServiceTests var badRequestException = await Assert.ThrowsAsync( () => sutProvider.Sut.SaveAsync(policy, - Substitute.For(), Substitute.For(), Guid.NewGuid())); @@ -125,7 +124,6 @@ public class PolicyServiceTests var badRequestException = await Assert.ThrowsAsync( () => sutProvider.Sut.SaveAsync(policy, - Substitute.For(), Substitute.For(), Guid.NewGuid())); @@ -163,7 +161,6 @@ public class PolicyServiceTests var badRequestException = await Assert.ThrowsAsync( () => sutProvider.Sut.SaveAsync(policy, - Substitute.For(), Substitute.For(), Guid.NewGuid())); @@ -192,7 +189,6 @@ public class PolicyServiceTests var badRequestException = await Assert.ThrowsAsync( () => sutProvider.Sut.SaveAsync(policy, - Substitute.For(), Substitute.For(), Guid.NewGuid())); @@ -226,7 +222,7 @@ public class PolicyServiceTests var utcNow = DateTime.UtcNow; - await sutProvider.Sut.SaveAsync(policy, Substitute.For(), Substitute.For(), Guid.NewGuid()); + await sutProvider.Sut.SaveAsync(policy, Substitute.For(), Guid.NewGuid()); await sutProvider.GetDependency().Received() .LogPolicyEventAsync(policy, EventType.Policy_Updated); @@ -256,7 +252,6 @@ public class PolicyServiceTests var badRequestException = await Assert.ThrowsAsync( () => sutProvider.Sut.SaveAsync(policy, - Substitute.For(), Substitute.For(), Guid.NewGuid())); @@ -348,34 +343,39 @@ public class PolicyServiceTests orgUserDetailAdmin }); - var userService = Substitute.For(); - var organizationService = Substitute.For(); + sutProvider.GetDependency() + .TwoFactorIsEnabledAsync(Arg.Any>()) + .Returns(new List<(OrganizationUserUserDetails user, bool hasTwoFactor)>() + { + (orgUserDetailUserInvited, false), + (orgUserDetailUserAcceptedWith2FA, true), + (orgUserDetailUserAcceptedWithout2FA, false), + (orgUserDetailAdmin, false), + }); - userService.TwoFactorIsEnabledAsync(orgUserDetailUserInvited).Returns(false); - userService.TwoFactorIsEnabledAsync(orgUserDetailUserAcceptedWith2FA).Returns(true); - userService.TwoFactorIsEnabledAsync(orgUserDetailUserAcceptedWithout2FA).Returns(false); - userService.TwoFactorIsEnabledAsync(orgUserDetailAdmin).Returns(false); + var organizationService = Substitute.For(); + var removeOrganizationUserCommand = sutProvider.GetDependency(); var utcNow = DateTime.UtcNow; var savingUserId = Guid.NewGuid(); - await sutProvider.Sut.SaveAsync(policy, userService, organizationService, savingUserId); + await sutProvider.Sut.SaveAsync(policy, organizationService, savingUserId); - await organizationService.Received() + await removeOrganizationUserCommand.Received() .RemoveUserAsync(policy.OrganizationId, orgUserDetailUserAcceptedWithout2FA.Id, savingUserId); await sutProvider.GetDependency().Received() .SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization.DisplayName(), orgUserDetailUserAcceptedWithout2FA.Email); - await organizationService.DidNotReceive() + await removeOrganizationUserCommand.DidNotReceive() .RemoveUserAsync(policy.OrganizationId, orgUserDetailUserInvited.Id, savingUserId); await sutProvider.GetDependency().DidNotReceive() .SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization.DisplayName(), orgUserDetailUserInvited.Email); - await organizationService.DidNotReceive() + await removeOrganizationUserCommand.DidNotReceive() .RemoveUserAsync(policy.OrganizationId, orgUserDetailUserAcceptedWith2FA.Id, savingUserId); await sutProvider.GetDependency().DidNotReceive() .SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization.DisplayName(), orgUserDetailUserAcceptedWith2FA.Email); - await organizationService.DidNotReceive() + await removeOrganizationUserCommand.DidNotReceive() .RemoveUserAsync(policy.OrganizationId, orgUserDetailAdmin.Id, savingUserId); await sutProvider.GetDependency().DidNotReceive() .SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization.DisplayName(), orgUserDetailAdmin.Email); @@ -456,21 +456,29 @@ public class PolicyServiceTests orgUserDetailAdmin }); - var userService = Substitute.For(); - var organizationService = Substitute.For(); + sutProvider.GetDependency() + .TwoFactorIsEnabledAsync(Arg.Is>(ids => + ids.Contains(orgUserDetailUserWith2FANoMP.UserId.Value) + && ids.Contains(orgUserDetailUserWithout2FA.UserId.Value) + && ids.Contains(orgUserDetailAdmin.UserId.Value))) + .Returns(new List<(Guid userId, bool hasTwoFactor)>() + { + (orgUserDetailUserWith2FANoMP.UserId.Value, true), + (orgUserDetailUserWithout2FA.UserId.Value, false), + (orgUserDetailAdmin.UserId.Value, false), + }); - userService.TwoFactorIsEnabledAsync(orgUserDetailUserWith2FANoMP).Returns(true); - userService.TwoFactorIsEnabledAsync(orgUserDetailUserWithout2FA).Returns(false); - userService.TwoFactorIsEnabledAsync(orgUserDetailAdmin).Returns(false); + var organizationService = Substitute.For(); + var removeOrganizationUserCommand = sutProvider.GetDependency(); var savingUserId = Guid.NewGuid(); var badRequestException = await Assert.ThrowsAsync( - () => sutProvider.Sut.SaveAsync(policy, userService, organizationService, savingUserId)); + () => sutProvider.Sut.SaveAsync(policy, organizationService, savingUserId)); Assert.Contains("Policy could not be enabled. Non-compliant members will lose access to their accounts. Identify members without two-step login from the policies column in the members page.", badRequestException.Message, StringComparison.OrdinalIgnoreCase); - await organizationService.DidNotReceiveWithAnyArgs() + await removeOrganizationUserCommand.DidNotReceiveWithAnyArgs() .RemoveUserAsync(organizationId: default, organizationUserId: default, deletingUserId: default); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() @@ -526,17 +534,20 @@ public class PolicyServiceTests orgUserDetail, }); - var userService = Substitute.For(); - var organizationService = Substitute.For(); + sutProvider.GetDependency() + .TwoFactorIsEnabledAsync(Arg.Is>(ids => ids.Contains(orgUserDetail.UserId.Value))) + .Returns(new List<(Guid userId, bool hasTwoFactor)>() + { + (orgUserDetail.UserId.Value, false), + }); - userService.TwoFactorIsEnabledAsync(orgUserDetail) - .Returns(false); + var organizationService = Substitute.For(); var utcNow = DateTime.UtcNow; var savingUserId = Guid.NewGuid(); - await sutProvider.Sut.SaveAsync(policy, userService, organizationService, savingUserId); + await sutProvider.Sut.SaveAsync(policy, organizationService, savingUserId); await sutProvider.GetDependency().Received() .LogPolicyEventAsync(policy, EventType.Policy_Updated); @@ -579,7 +590,6 @@ public class PolicyServiceTests var badRequestException = await Assert.ThrowsAsync( () => sutProvider.Sut.SaveAsync(policy, - Substitute.For(), Substitute.For(), Guid.NewGuid())); @@ -616,7 +626,6 @@ public class PolicyServiceTests var badRequestException = await Assert.ThrowsAsync( () => sutProvider.Sut.SaveAsync(policy, - Substitute.For(), Substitute.For(), Guid.NewGuid())); @@ -650,7 +659,6 @@ public class PolicyServiceTests var badRequestException = await Assert.ThrowsAsync( () => sutProvider.Sut.SaveAsync(policy, - Substitute.For(), Substitute.For(), Guid.NewGuid())); @@ -684,7 +692,6 @@ public class PolicyServiceTests var badRequestException = await Assert.ThrowsAsync( () => sutProvider.Sut.SaveAsync(policy, - Substitute.For(), Substitute.For(), Guid.NewGuid())); diff --git a/test/Core.Test/Auth/Services/SsoConfigServiceTests.cs b/test/Core.Test/Auth/Services/SsoConfigServiceTests.cs index 9c63c6b01..fb566537a 100644 --- a/test/Core.Test/Auth/Services/SsoConfigServiceTests.cs +++ b/test/Core.Test/Auth/Services/SsoConfigServiceTests.cs @@ -342,7 +342,6 @@ public class SsoConfigServiceTests await sutProvider.GetDependency().Received(1) .SaveAsync( Arg.Is(t => t.Type == PolicyType.SingleOrg), - Arg.Any(), Arg.Any(), null ); @@ -350,7 +349,6 @@ public class SsoConfigServiceTests await sutProvider.GetDependency().Received(1) .SaveAsync( Arg.Is(t => t.Type == PolicyType.ResetPassword && t.GetDataModel().AutoEnrollEnabled), - Arg.Any(), Arg.Any(), null ); diff --git a/test/Core.Test/Billing/Services/PaymentHistoryServiceTests.cs b/test/Core.Test/Billing/Services/PaymentHistoryServiceTests.cs index 34bbb368b..c9278e448 100644 --- a/test/Core.Test/Billing/Services/PaymentHistoryServiceTests.cs +++ b/test/Core.Test/Billing/Services/PaymentHistoryServiceTests.cs @@ -29,7 +29,7 @@ public class PaymentHistoryServiceTests var result = await paymentHistoryService.GetInvoiceHistoryAsync(subscriber); // Assert - Assert.NotNull(result); + Assert.NotEmpty(result); Assert.Single(result); await stripeAdapter.Received(1).InvoiceListAsync(Arg.Any()); } @@ -47,7 +47,7 @@ public class PaymentHistoryServiceTests var result = await paymentHistoryService.GetInvoiceHistoryAsync(null); // Assert - Assert.Null(result); + Assert.Empty(result); } [Fact] @@ -66,7 +66,7 @@ public class PaymentHistoryServiceTests var result = await paymentHistoryService.GetTransactionHistoryAsync(subscriber); // Assert - Assert.NotNull(result); + Assert.NotEmpty(result); Assert.Single(result); await transactionRepository.Received(1).GetManyByOrganizationIdAsync(subscriber.Id, Arg.Any(), Arg.Any()); } @@ -84,6 +84,6 @@ public class PaymentHistoryServiceTests var result = await paymentHistoryService.GetTransactionHistoryAsync(null); // Assert - Assert.Null(result); + Assert.Empty(result); } } diff --git a/test/Core.Test/Core.Test.csproj b/test/Core.Test/Core.Test.csproj index a7aaa2302..4858afe54 100644 --- a/test/Core.Test/Core.Test.csproj +++ b/test/Core.Test/Core.Test.csproj @@ -17,7 +17,7 @@ - + diff --git a/test/Core.Test/Services/UserServiceTests.cs b/test/Core.Test/Services/UserServiceTests.cs index 098d4d279..480c7b639 100644 --- a/test/Core.Test/Services/UserServiceTests.cs +++ b/test/Core.Test/Services/UserServiceTests.cs @@ -1,5 +1,6 @@ using System.Text.Json; using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Enums; @@ -263,7 +264,8 @@ public class UserServiceTests sutProvider.GetDependency(), new FakeDataProtectorTokenFactory(), sutProvider.GetDependency(), - sutProvider.GetDependency() + sutProvider.GetDependency(), + sutProvider.GetDependency() ); var actualIsVerified = await sut.VerifySecretAsync(user, secret); diff --git a/test/Identity.IntegrationTest/RequestValidation/ResourceOwnerPasswordValidatorTests.cs b/test/Identity.IntegrationTest/RequestValidation/ResourceOwnerPasswordValidatorTests.cs index fac271b14..91d0ee01f 100644 --- a/test/Identity.IntegrationTest/RequestValidation/ResourceOwnerPasswordValidatorTests.cs +++ b/test/Identity.IntegrationTest/RequestValidation/ResourceOwnerPasswordValidatorTests.cs @@ -4,11 +4,15 @@ using Bit.Core.Auth.Enums; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Identity.IdentityServer; using Bit.Identity.Models.Request.Accounts; using Bit.IntegrationTestCommon.Factories; using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.Helpers; +using Duende.IdentityServer.Validation; using Microsoft.AspNetCore.Identity; +using NSubstitute; using Xunit; namespace Bit.Identity.IntegrationTest.RequestValidation; @@ -21,6 +25,7 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture _userManager; private readonly IAuthRequestRepository _authRequestRepository; + private readonly IDeviceService _deviceService; public ResourceOwnerPasswordValidatorTests(IdentityApplicationFactory factory) { @@ -28,13 +33,13 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture>(); _authRequestRepository = _factory.GetService(); - + _deviceService = _factory.GetService(); } [Fact] public async Task ValidateAsync_Success() { - // Arrange + // Arrange await EnsureUserCreatedAsync(); // Act @@ -53,7 +58,7 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture - /// I would have liked to spy into the IUserService but by spying into the IUserService it - /// creates a Singleton that is not available to the UserManager thus causing the + /// I would have liked to spy into the IUserService but by spying into the IUserService it + /// creates a Singleton that is not available to the UserManager thus causing the /// RegisterAsync() to create a the user in a different UserStore than the one the - /// UserManager has access to. This is an assumption made from observing the behavior while - /// writing theses tests. I could be wrong. - /// + /// UserManager has access to (This is an assumption made from observing the behavior while + /// writing theses tests, I could be wrong). + /// /// For the time being, verifying that the user is not null confirms that the failure is due to /// a bad password. /// @@ -102,10 +107,11 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture context.SetAuthEmail(DefaultUsername)); // Assert - Assert.NotNull(await _userManager.FindByEmailAsync(DefaultUsername)); - var body = await AssertHelper.AssertResponseTypeIs(context); var root = body.RootElement; @@ -200,8 +204,8 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture(sub => + { + sub.SaveDeviceAsync(Arg.Any(), Arg.Any()) + .Returns(null as Device); + }); + + // Add User + await factory.RegisterAsync(new RegisterRequestModel + { + Email = DefaultUsername, + MasterPasswordHash = DefaultPassword + }); + var userManager = factory.GetService>(); + var user = await userManager.FindByEmailAsync(DefaultUsername); + Assert.NotNull(user); + + // Act + var context = await factory.Server.PostAsync("/connect/token", + GetFormUrlEncodedContent(), + context => context.SetAuthEmail(DefaultUsername)); + + // Assert + var body = await AssertHelper.AssertResponseTypeIs(context); + var root = body.RootElement; + + var errorModel = AssertHelper.AssertJsonProperty(root, "ErrorModel", JsonValueKind.Object); + var errorMessage = AssertHelper.AssertJsonProperty(errorModel, "Message", JsonValueKind.String).GetString(); + Assert.Equal("No device information provided.", errorMessage); + } + private async Task EnsureUserCreatedAsync(IdentityApplicationFactory factory = null) { factory ??= _factory; diff --git a/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs b/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs index c1d34e1b0..39b7edf8d 100644 --- a/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs @@ -29,10 +29,9 @@ namespace Bit.Identity.Test.IdentityServer; public class BaseRequestValidatorTests { private UserManager _userManager; - private readonly IDeviceRepository _deviceRepository; - private readonly IDeviceService _deviceService; private readonly IUserService _userService; private readonly IEventService _eventService; + private readonly IDeviceValidator _deviceValidator; private readonly IOrganizationDuoWebTokenProvider _organizationDuoWebTokenProvider; private readonly ITemporaryDuoWebV4SDKService _duoWebV4SDKService; private readonly IOrganizationRepository _organizationRepository; @@ -53,10 +52,9 @@ public class BaseRequestValidatorTests public BaseRequestValidatorTests() { - _deviceRepository = Substitute.For(); - _deviceService = Substitute.For(); _userService = Substitute.For(); _eventService = Substitute.For(); + _deviceValidator = Substitute.For(); _organizationDuoWebTokenProvider = Substitute.For(); _duoWebV4SDKService = Substitute.For(); _organizationRepository = Substitute.For(); @@ -76,10 +74,9 @@ public class BaseRequestValidatorTests _sut = new BaseRequestValidatorTestWrapper( _userManager, - _deviceRepository, - _deviceService, _userService, _eventService, + _deviceValidator, _organizationDuoWebTokenProvider, _duoWebV4SDKService, _organizationRepository, @@ -228,7 +225,8 @@ public class BaseRequestValidatorTests public async Task ValidateAsync_ClientCredentialsGrantType_ShouldSucceed( [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, CustomValidatorRequestContext requestContext, - GrantValidationResult grantResult) + GrantValidationResult grantResult, + Device device) { // Arrange var context = CreateContext(tokenRequest, requestContext, grantResult); @@ -240,18 +238,13 @@ public class BaseRequestValidatorTests _globalSettings.DisableEmailNewDevice = false; context.ValidatedTokenRequest.GrantType = "client_credentials"; // This || AuthCode will allow process to continue to get device - context.ValidatedTokenRequest.Raw["DeviceIdentifier"] = "DeviceIdentifier"; - context.ValidatedTokenRequest.Raw["DeviceType"] = "Android"; // This needs to be an actual Type - context.ValidatedTokenRequest.Raw["DeviceName"] = "DeviceName"; - context.ValidatedTokenRequest.Raw["DevicePushToken"] = "DevicePushToken"; + _deviceValidator.SaveDeviceAsync(Arg.Any(), Arg.Any()) + .Returns(device); // Act await _sut.ValidateAsync(context); // Assert - await _mailService.Received(1).SendNewDeviceLoggedInEmail( - context.CustomValidatorRequestContext.User.Email, "Android", Arg.Any(), Arg.Any() - ); Assert.False(context.GrantResult.IsError); } @@ -262,7 +255,8 @@ public class BaseRequestValidatorTests public async Task ValidateAsync_ClientCredentialsGrantType_ExistingDevice_ShouldSucceed( [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, CustomValidatorRequestContext requestContext, - GrantValidationResult grantResult) + GrantValidationResult grantResult, + Device device) { // Arrange var context = CreateContext(tokenRequest, requestContext, grantResult); @@ -274,13 +268,9 @@ public class BaseRequestValidatorTests _globalSettings.DisableEmailNewDevice = false; context.ValidatedTokenRequest.GrantType = "client_credentials"; // This || AuthCode will allow process to continue to get device - context.ValidatedTokenRequest.Raw["DeviceIdentifier"] = "DeviceIdentifier"; - context.ValidatedTokenRequest.Raw["DeviceType"] = "Android"; // This needs to be an actual Type - context.ValidatedTokenRequest.Raw["DeviceName"] = "DeviceName"; - context.ValidatedTokenRequest.Raw["DevicePushToken"] = "DevicePushToken"; - _deviceRepository.GetByIdentifierAsync("DeviceIdentifier", Arg.Any()) - .Returns(new Device() { Identifier = "DeviceIdentifier" }); + _deviceValidator.SaveDeviceAsync(Arg.Any(), Arg.Any()) + .Returns(device); // Act await _sut.ValidateAsync(context); diff --git a/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs b/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs new file mode 100644 index 000000000..1f4d5a807 --- /dev/null +++ b/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs @@ -0,0 +1,247 @@ +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Settings; +using Bit.Identity.IdentityServer; +using Bit.Test.Common.AutoFixture.Attributes; +using Duende.IdentityServer.Validation; +using NSubstitute; +using Xunit; +using AuthFixtures = Bit.Identity.Test.AutoFixture; + +namespace Bit.Identity.Test.IdentityServer; + +public class DeviceValidatorTests +{ + private readonly IDeviceService _deviceService; + private readonly IDeviceRepository _deviceRepository; + private readonly GlobalSettings _globalSettings; + private readonly IMailService _mailService; + private readonly ICurrentContext _currentContext; + private readonly DeviceValidator _sut; + + public DeviceValidatorTests() + { + _deviceService = Substitute.For(); + _deviceRepository = Substitute.For(); + _globalSettings = new GlobalSettings(); + _mailService = Substitute.For(); + _currentContext = Substitute.For(); + _sut = new DeviceValidator( + _deviceService, + _deviceRepository, + _globalSettings, + _mailService, + _currentContext); + } + + [Theory] + [BitAutoData] + public async void SaveDeviceAsync_DeviceNull_ShouldReturnNull( + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request, + User user) + { + // Arrange + request.Raw["DeviceIdentifier"] = null; + + // Act + var device = await _sut.SaveDeviceAsync(user, request); + + // Assert + Assert.Null(device); + await _mailService.DidNotReceive().SendNewDeviceLoggedInEmail( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Theory] + [BitAutoData] + public async void SaveDeviceAsync_UserIsNull_ShouldReturnNull( + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request) + { + // Arrange + request = AddValidDeviceToRequest(request); + + // Act + var device = await _sut.SaveDeviceAsync(null, request); + + // Assert + Assert.Null(device); + await _mailService.DidNotReceive().SendNewDeviceLoggedInEmail( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Theory] + [BitAutoData] + public async void SaveDeviceAsync_ExistingUser_NewDevice_ReturnsDevice_SendsEmail( + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request, + User user) + { + // Arrange + request = AddValidDeviceToRequest(request); + + user.CreationDate = DateTime.UtcNow - TimeSpan.FromMinutes(11); + _globalSettings.DisableEmailNewDevice = false; + + // Act + var device = await _sut.SaveDeviceAsync(user, request); + + // Assert + Assert.NotNull(device); + Assert.Equal(user.Id, device.UserId); + Assert.Equal("DeviceIdentifier", device.Identifier); + Assert.Equal(DeviceType.Android, device.Type); + await _mailService.Received(1).SendNewDeviceLoggedInEmail( + user.Email, "Android", Arg.Any(), Arg.Any()); + } + + [Theory] + [BitAutoData] + public async void SaveDeviceAsync_ExistingUser_NewDevice_ReturnsDevice_SendEmailFalse( + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request, + User user) + { + // Arrange + request = AddValidDeviceToRequest(request); + + user.CreationDate = DateTime.UtcNow - TimeSpan.FromMinutes(11); + _globalSettings.DisableEmailNewDevice = true; + + // Act + var device = await _sut.SaveDeviceAsync(user, request); + + // Assert + Assert.NotNull(device); + Assert.Equal(user.Id, device.UserId); + Assert.Equal("DeviceIdentifier", device.Identifier); + Assert.Equal(DeviceType.Android, device.Type); + await _mailService.DidNotReceive().SendNewDeviceLoggedInEmail( + user.Email, "Android", Arg.Any(), Arg.Any()); + } + + [Theory] + [BitAutoData] + public async void SaveDeviceAsync_DeviceIsKnown_ShouldReturnDevice( + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request, + User user, + Device device) + { + // Arrange + request = AddValidDeviceToRequest(request); + + device.UserId = user.Id; + device.Identifier = "DeviceIdentifier"; + device.Type = DeviceType.Android; + device.Name = "DeviceName"; + device.PushToken = "DevicePushToken"; + _deviceRepository.GetByIdentifierAsync(device.Identifier, user.Id).Returns(device); + + // Act + var resultDevice = await _sut.SaveDeviceAsync(user, request); + + // Assert + Assert.Equal(device, resultDevice); + await _mailService.DidNotReceive().SendNewDeviceLoggedInEmail( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Theory] + [BitAutoData] + public async void SaveDeviceAsync_NewUser_DeviceUnknown_ShouldSaveDevice_NoEmail( + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request, + User user) + { + // Arrange + request = AddValidDeviceToRequest(request); + user.CreationDate = DateTime.UtcNow; + _deviceRepository.GetByIdentifierAsync(Arg.Any(), Arg.Any()).Returns(null as Device); + + // Act + var device = await _sut.SaveDeviceAsync(user, request); + + // Assert + Assert.NotNull(device); + Assert.Equal(user.Id, device.UserId); + Assert.Equal("DeviceIdentifier", device.Identifier); + Assert.Equal(DeviceType.Android, device.Type); + await _deviceService.Received(1).SaveAsync(device); + await _mailService.DidNotReceive().SendNewDeviceLoggedInEmail( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Theory] + [BitAutoData] + public async void KnownDeviceAsync_UserNull_ReturnsFalse( + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request) + { + // Arrange + request = AddValidDeviceToRequest(request); + + // Act + var result = await _sut.KnownDeviceAsync(null, request); + + // Assert + Assert.False(result); + } + + [Theory] + [BitAutoData] + public async void KnownDeviceAsync_DeviceNull_ReturnsFalse( + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request, + User user) + { + // Arrange + // Device raw data is null which will cause the device to be null + + // Act + var result = await _sut.KnownDeviceAsync(user, request); + + // Assert + Assert.False(result); + } + + [Theory] + [BitAutoData] + public async void KnownDeviceAsync_DeviceNotInDatabase_ReturnsFalse( + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request, + User user) + { + // Arrange + request = AddValidDeviceToRequest(request); + _deviceRepository.GetByIdentifierAsync(Arg.Any(), Arg.Any()) + .Returns(null as Device); + // Act + var result = await _sut.KnownDeviceAsync(user, request); + + // Assert + Assert.False(result); + } + + [Theory] + [BitAutoData] + public async void KnownDeviceAsync_UserAndDeviceValid_ReturnsTrue( + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request, + User user, + Device device) + { + // Arrange + request = AddValidDeviceToRequest(request); + _deviceRepository.GetByIdentifierAsync(Arg.Any(), Arg.Any()) + .Returns(device); + // Act + var result = await _sut.KnownDeviceAsync(user, request); + + // Assert + Assert.True(result); + } + + private ValidatedTokenRequest AddValidDeviceToRequest(ValidatedTokenRequest request) + { + request.Raw["DeviceIdentifier"] = "DeviceIdentifier"; + request.Raw["DeviceType"] = "Android"; + request.Raw["DeviceName"] = "DeviceName"; + request.Raw["DevicePushToken"] = "DevicePushToken"; + return request; + } +} diff --git a/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs b/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs index e525d0de7..26043fd59 100644 --- a/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs +++ b/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs @@ -51,10 +51,9 @@ IBaseRequestValidatorTestWrapper public bool isValid { get; set; } public BaseRequestValidatorTestWrapper( UserManager userManager, - IDeviceRepository deviceRepository, - IDeviceService deviceService, IUserService userService, IEventService eventService, + IDeviceValidator deviceValidator, IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider, ITemporaryDuoWebV4SDKService duoWebV4SDKService, IOrganizationRepository organizationRepository, @@ -72,10 +71,9 @@ IBaseRequestValidatorTestWrapper IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder) : base( userManager, - deviceRepository, - deviceService, userService, eventService, + deviceValidator, organizationDuoWebTokenProvider, duoWebV4SDKService, organizationRepository, diff --git a/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs b/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs index e0fcc0e5e..aafe86d56 100644 --- a/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs +++ b/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs @@ -145,7 +145,10 @@ public abstract class WebApplicationFactoryBase : WebApplicationFactory // Email Verification { "globalSettings:enableEmailVerification", "true" }, { "globalSettings:disableUserRegistration", "false" }, - { "globalSettings:launchDarkly:flagValues:email-verification", "true" } + { "globalSettings:launchDarkly:flagValues:email-verification", "true" }, + + // New Device Verification + { "globalSettings:disableEmailNewDevice", "false" }, }); }); diff --git a/util/Migrator/DbMigrator.cs b/util/Migrator/DbMigrator.cs index 11b80fac7..b9584326e 100644 --- a/util/Migrator/DbMigrator.cs +++ b/util/Migrator/DbMigrator.cs @@ -14,13 +14,15 @@ public class DbMigrator private readonly string _connectionString; private readonly ILogger _logger; private readonly bool _skipDatabasePreparation; + private readonly bool _noTransactionMigration; public DbMigrator(string connectionString, ILogger logger = null, - bool skipDatabasePreparation = false) + bool skipDatabasePreparation = false, bool noTransactionMigration = false) { _connectionString = connectionString; _logger = logger ?? CreateLogger(); _skipDatabasePreparation = skipDatabasePreparation; + _noTransactionMigration = noTransactionMigration; } public bool MigrateMsSqlDatabaseWithRetries(bool enableLogging = true, @@ -30,6 +32,7 @@ public class DbMigrator CancellationToken cancellationToken = default) { var attempt = 1; + while (attempt < 10) { try @@ -69,6 +72,7 @@ public class DbMigrator using (var connection = new SqlConnection(masterConnectionString)) { var databaseName = new SqlConnectionStringBuilder(_connectionString).InitialCatalog; + if (string.IsNullOrWhiteSpace(databaseName)) { databaseName = "vault"; @@ -105,10 +109,10 @@ public class DbMigrator } private bool MigrateDatabase(bool enableLogging = true, - bool repeatable = false, - string folderName = MigratorConstants.DefaultMigrationsFolderName, - bool dryRun = false, - CancellationToken cancellationToken = default) + bool repeatable = false, + string folderName = MigratorConstants.DefaultMigrationsFolderName, + bool dryRun = false, + CancellationToken cancellationToken = default) { if (enableLogging) { @@ -121,8 +125,17 @@ public class DbMigrator .SqlDatabase(_connectionString) .WithScriptsAndCodeEmbeddedInAssembly(Assembly.GetExecutingAssembly(), s => s.Contains($".{folderName}.") && !s.Contains(".Archive.")) - .WithTransaction() - .WithExecutionTimeout(new TimeSpan(0, 5, 0)); + .WithExecutionTimeout(TimeSpan.FromMinutes(5)); + + if (_noTransactionMigration) + { + builder = builder.WithoutTransaction() + .WithExecutionTimeout(TimeSpan.FromMinutes(60)); + } + else + { + builder = builder.WithTransaction(); + } if (repeatable) { @@ -144,6 +157,7 @@ public class DbMigrator { var scriptsToExec = upgrader.GetScriptsToExecute(); var stringBuilder = new StringBuilder("Scripts that will be applied:"); + foreach (var script in scriptsToExec) { stringBuilder.AppendLine(script.Name); diff --git a/util/Migrator/DbScripts/2024-09-26_00_AddVerifiedOrganizationDomainSsoDetails_ReadByEmail.sql b/util/Migrator/DbScripts/2024-09-26_00_AddVerifiedOrganizationDomainSsoDetails_ReadByEmail.sql new file mode 100644 index 000000000..e36ea1f46 --- /dev/null +++ b/util/Migrator/DbScripts/2024-09-26_00_AddVerifiedOrganizationDomainSsoDetails_ReadByEmail.sql @@ -0,0 +1,24 @@ +CREATE OR ALTER PROCEDURE [dbo].[VerifiedOrganizationDomainSsoDetails_ReadByEmail] + @Email NVARCHAR(256) +AS +BEGIN + SET NOCOUNT ON + + DECLARE @Domain NVARCHAR(256) + +SELECT @Domain = SUBSTRING(@Email, CHARINDEX( '@', @Email) + 1, LEN(@Email)) + +SELECT + O.Id AS OrganizationId, + O.Name AS OrganizationName, + O.Identifier AS OrganizationIdentifier, + OD.DomainName +FROM [dbo].[OrganizationView] O + INNER JOIN [dbo].[OrganizationDomainView] OD ON O.Id = OD.OrganizationId + LEFT JOIN [dbo].[Ssoconfig] S ON O.Id = S.OrganizationId +WHERE OD.DomainName = @Domain + AND O.Enabled = 1 + AND OD.VerifiedDate IS NOT NULL + AND S.Enabled = 1 +END +GO diff --git a/util/MsSqlMigratorUtility/Program.cs b/util/MsSqlMigratorUtility/Program.cs index 03e4716e0..056cb696f 100644 --- a/util/MsSqlMigratorUtility/Program.cs +++ b/util/MsSqlMigratorUtility/Program.cs @@ -17,13 +17,15 @@ internal class Program [Option('f', "folder", Description = "Folder name of database scripts")] string folderName = MigratorConstants.DefaultMigrationsFolderName, [Option('d', "dry-run", Description = "Print the scripts that will be applied without actually executing them")] - bool dryRun = false - ) => MigrateDatabase(databaseConnectionString, repeatable, folderName, dryRun); + bool dryRun = false, + [Option("no-transaction", Description = "Run without adding transaction per script or all scripts")] + bool noTransactionMigration = false + ) => MigrateDatabase(databaseConnectionString, repeatable, folderName, dryRun, noTransactionMigration); private static bool MigrateDatabase(string databaseConnectionString, - bool repeatable = false, string folderName = "", bool dryRun = false) + bool repeatable = false, string folderName = "", bool dryRun = false, bool noTransactionMigration = false) { - var migrator = new DbMigrator(databaseConnectionString); + var migrator = new DbMigrator(databaseConnectionString, noTransactionMigration: noTransactionMigration); bool success; if (!string.IsNullOrWhiteSpace(folderName)) {