diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3aaa8f517b..7562dd354a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,6 +10,9 @@ on: - ".github/workflows/**" workflow_dispatch: +env: + _AZ_REGISTRY: "bitwardenprod.azurecr.io" + jobs: cloc: name: CLOC @@ -33,6 +36,9 @@ jobs: - name: Checkout repo uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - name: Set up dotnet + uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0 + - name: Verify Format run: dotnet format --verify-no-changes @@ -42,10 +48,11 @@ jobs: env: NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages steps: + - name: Checkout repo + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - name: Set up dotnet uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0 - with: - dotnet-version: "6.0.x" - name: Print environment run: | @@ -54,9 +61,6 @@ jobs: echo "GitHub ref: $GITHUB_REF" echo "GitHub event: $GITHUB_EVENT" - - name: Checkout repo - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - - name: Restore run: dotnet restore --locked-mode shell: pwsh @@ -114,8 +118,14 @@ jobs: base_path: ./src - project_name: Identity base_path: ./src + - project_name: MsSqlMigratorUtility + base_path: ./util + dotnet: true - project_name: Notifications base_path: ./src + - project_name: Scim + base_path: ./bitwarden_license/src + dotnet: true - project_name: Server base_path: ./util - project_name: Setup @@ -123,16 +133,13 @@ jobs: - project_name: Sso base_path: ./bitwarden_license/src node: true - - project_name: Scim - base_path: ./bitwarden_license/src - dotnet: true - - project_name: MsSqlMigratorUtility - base_path: ./util - dotnet: true steps: - name: Checkout repo uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - name: Set up dotnet + uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0 + - name: Set up Node uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0 with: @@ -194,64 +201,48 @@ jobs: include: - project_name: Admin base_path: ./src - docker_repos: [bitwarden, bitwardenprod.azurecr.io, bitwardenqa.azurecr.io] dotnet: true - project_name: Api base_path: ./src - docker_repos: [bitwarden, bitwardenprod.azurecr.io, bitwardenqa.azurecr.io] dotnet: true - project_name: Attachments base_path: ./util - docker_repos: [bitwarden, bitwardenprod.azurecr.io, bitwardenqa.azurecr.io] + - project_name: Billing + base_path: ./src + dotnet: true - project_name: Events base_path: ./src - docker_repos: [bitwarden, bitwardenprod.azurecr.io, bitwardenqa.azurecr.io] dotnet: true - project_name: EventsProcessor base_path: ./src - docker_repos: [bitwardenprod.azurecr.io, bitwardenqa.azurecr.io] dotnet: true - project_name: Icons base_path: ./src - docker_repos: [bitwarden, bitwardenprod.azurecr.io, bitwardenqa.azurecr.io] dotnet: true - project_name: Identity base_path: ./src - docker_repos: [bitwarden, bitwardenprod.azurecr.io, bitwardenqa.azurecr.io] dotnet: true - project_name: MsSql base_path: ./util - docker_repos: [bitwarden, bitwardenprod.azurecr.io, bitwardenqa.azurecr.io] + - project_name: MsSqlMigratorUtility + base_path: ./util + dotnet: true - project_name: Nginx base_path: ./util - docker_repos: [bitwarden, bitwardenprod.azurecr.io, bitwardenqa.azurecr.io] - project_name: Notifications base_path: ./src - docker_repos: [bitwarden, bitwardenprod.azurecr.io, bitwardenqa.azurecr.io] - dotnet: true - - project_name: Server - base_path: ./util - docker_repos: [bitwarden, bitwardenprod.azurecr.io, bitwardenqa.azurecr.io] - dotnet: true - - project_name: Setup - base_path: ./util - docker_repos: [bitwarden, bitwardenprod.azurecr.io, bitwardenqa.azurecr.io] - dotnet: true - - project_name: Sso - base_path: ./bitwarden_license/src - docker_repos: [bitwarden, bitwardenprod.azurecr.io, bitwardenqa.azurecr.io] dotnet: true - project_name: Scim base_path: ./bitwarden_license/src - docker_repos: [bitwarden, bitwardenprod.azurecr.io, bitwardenqa.azurecr.io] dotnet: true - - project_name: Billing - base_path: ./src - docker_repos: [bitwardenprod.azurecr.io, bitwardenqa.azurecr.io] - dotnet: true - - project_name: MsSqlMigratorUtility + - project_name: Server base_path: ./util - docker_repos: [bitwardenprod.azurecr.io, bitwardenqa.azurecr.io] + dotnet: true + - project_name: Setup + base_path: ./util + dotnet: true + - project_name: Sso + base_path: ./bitwarden_license/src dotnet: true steps: - name: Checkout repo @@ -271,14 +262,6 @@ jobs: fi ########## ACRs ########## - - name: Login to Azure - QA Subscription - uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7 - with: - creds: ${{ secrets.AZURE_QA_KV_CREDENTIALS }} - - - name: Login to QA ACR - run: az acr login -n bitwardenqa - - name: Login to Azure - PROD Subscription uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7 with: @@ -294,36 +277,11 @@ jobs: - name: Retrieve github PAT secrets id: retrieve-secret-pat - uses: bitwarden/gh-actions/get-keyvault-secrets@74f4ac01c9abe0a7331c9a5de822a558fd4a4710 + uses: bitwarden/gh-actions/get-keyvault-secrets@f096207b7a2f31723165aee6ad03e91716686e78 with: keyvault: "bitwarden-ci" secrets: "github-pat-bitwarden-devops-bot-repo-scope" - - name: Retrieve secrets - if: ${{ env.is_publish_branch == 'true' }} - id: retrieve-secrets - uses: bitwarden/gh-actions/get-keyvault-secrets@74f4ac01c9abe0a7331c9a5de822a558fd4a4710 - with: - keyvault: "bitwarden-ci" - secrets: "docker-password, - docker-username, - dct-delegate-2-repo-passphrase, - dct-delegate-2-key" - - - name: Log into Docker - if: ${{ env.is_publish_branch == 'true' }} - env: - DOCKER_USERNAME: ${{ steps.retrieve-secrets.outputs.docker-username }} - DOCKER_PASSWORD: ${{ steps.retrieve-secrets.outputs.docker-password }} - run: echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin - - - name: Setup Docker Content Trust (DCT) - if: ${{ env.is_publish_branch == 'true' }} - uses: bitwarden/gh-actions/setup-docker-trust@74f4ac01c9abe0a7331c9a5de822a558fd4a4710 - with: - azure-creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - azure-keyvault-name: "bitwarden-ci" - ########## Generate image tag and build Docker image ########## - name: Generate Docker image tag id: tag @@ -342,12 +300,12 @@ jobs: echo "PROJECT_NAME: $PROJECT_NAME" echo "project_name=$PROJECT_NAME" >> $GITHUB_OUTPUT - - name: Generate tag list - id: tag-list + - name: Generate image full name + id: image-name env: IMAGE_TAG: ${{ steps.tag.outputs.image_tag }} PROJECT_NAME: ${{ steps.setup.outputs.project_name }} - run: echo "tags=bitwardenqa.azurecr.io/${PROJECT_NAME}:${IMAGE_TAG},bitwardenprod.azurecr.io/${PROJECT_NAME}:${IMAGE_TAG}" >> $GITHUB_OUTPUT + run: echo "name=${_AZ_REGISTRY}/${PROJECT_NAME}:${IMAGE_TAG}" >> $GITHUB_OUTPUT - name: Get build artifact if: ${{ matrix.dotnet }} @@ -369,37 +327,29 @@ jobs: file: ${{ matrix.base_path }}/${{ matrix.project_name }}/Dockerfile platforms: linux/amd64 push: true - tags: ${{ steps.tag-list.outputs.tags }} + tags: ${{ steps.image-name.outputs.name }} secrets: | "GH_PAT=${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }}" - - name: Push to DockerHub - if: contains(matrix.docker_repos, 'bitwarden') && env.is_publish_branch == 'true' - env: - IMAGE_TAG: ${{ steps.tag.outputs.image_tag }} - PROJECT_NAME: ${{ steps.setup.outputs.project_name }} - run: | - docker tag bitwardenprod.azurecr.io/$PROJECT_NAME:$IMAGE_TAG bitwarden/$PROJECT_NAME:$IMAGE_TAG - docker push bitwarden/$PROJECT_NAME:$IMAGE_TAG - - - name: Log out of Docker - run: | - docker logout - echo "DOCKER_CONTENT_TRUST=0" >> $GITHUB_ENV - upload: name: Upload runs-on: ubuntu-22.04 needs: build-docker steps: - - name: Set up dotnet - uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0 - with: - dotnet-version: "6.0.x" - - name: Checkout repo uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - name: Set up dotnet + uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0 + + - name: Login to Azure - PROD Subscription + uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7 + with: + creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }} + + - name: Login to PROD ACR + run: az acr login -n $_AZ_REGISTRY --only-show-errors + - name: Restore run: dotnet tool restore @@ -408,14 +358,18 @@ jobs: github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc' run: | - # Set proper image based on branch - if [[ "${{ github.ref }}" == "rc" ]]; then - SETUP_IMAGE="bitwarden/setup:rc" - elif [[ "${{ github.ref }}" == "hotfix-rc" ]]; then - SETUP_IMAGE="bitwarden/setup:hotfix-rc" - else - SETUP_IMAGE="bitwarden/setup:dev" - fi + # Set proper setup image based on branch + case "${{ github.ref }}" in + "refs/heads/master") + SETUP_IMAGE="$_AZ_REGISTRY/setup:dev" + ;; + "refs/heads/rc") + SETUP_IMAGE="$_AZ_REGISTRY/setup:rc" + ;; + "refs/heads/hotfix-rc") + SETUP_IMAGE="$_AZ_REGISTRY/setup:hotfix-rc" + ;; + esac STUB_OUTPUT=$(pwd)/docker-stub @@ -508,8 +462,7 @@ jobs: build-mssqlmigratorutility: name: Build MsSqlMigratorUtility runs-on: ubuntu-22.04 - needs: - - lint + needs: lint defaults: run: shell: bash @@ -521,11 +474,13 @@ jobs: - osx-x64 - linux-x64 - win-x64 - steps: - name: Checkout repo uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - name: Set up dotnet + uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0 + - name: Print environment run: | whoami @@ -539,7 +494,9 @@ jobs: dotnet restore - name: Publish project - run: dotnet publish -c "Release" -o obj/build-output/publish -r ${{ matrix.target }} -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true --self-contained true + run: | + dotnet publish -c "Release" -o obj/build-output/publish -r ${{ matrix.target }} -p:PublishSingleFile=true \ + -p:IncludeNativeLibrariesForSelfExtract=true --self-contained true - name: Upload project artifact Windows if: ${{ contains(matrix.target, 'win') == true }} @@ -620,7 +577,7 @@ jobs: - name: Retrieve secrets id: retrieve-secrets - uses: bitwarden/gh-actions/get-keyvault-secrets@74f4ac01c9abe0a7331c9a5de822a558fd4a4710 + uses: bitwarden/gh-actions/get-keyvault-secrets@f096207b7a2f31723165aee6ad03e91716686e78 if: failure() with: keyvault: "bitwarden-ci" diff --git a/.github/workflows/container-registry-purge.yml b/.github/workflows/container-registry-purge.yml index e6712acf77..622c614c80 100644 --- a/.github/workflows/container-registry-purge.yml +++ b/.github/workflows/container-registry-purge.yml @@ -92,7 +92,7 @@ jobs: - name: Retrieve secrets id: retrieve-secrets - uses: bitwarden/gh-actions/get-keyvault-secrets@74f4ac01c9abe0a7331c9a5de822a558fd4a4710 + uses: bitwarden/gh-actions/get-keyvault-secrets@f096207b7a2f31723165aee6ad03e91716686e78 if: failure() with: keyvault: "bitwarden-ci" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b2b0291870..e2ce751bc5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,6 +15,9 @@ on: - Redeploy - Dry Run +env: + _AZ_REGISTRY: 'bitwardenprod.azurecr.io' + jobs: setup: name: Setup @@ -38,7 +41,7 @@ jobs: - name: Check Release Version id: version - uses: bitwarden/gh-actions/release-version-check@74f4ac01c9abe0a7331c9a5de822a558fd4a4710 + uses: bitwarden/gh-actions/release-version-check@f096207b7a2f31723165aee6ad03e91716686e78 with: release-type: ${{ github.event.inputs.release_type }} project-type: dotnet @@ -53,18 +56,17 @@ jobs: deploy: name: Deploy runs-on: ubuntu-22.04 - needs: - - setup + needs: setup strategy: fail-fast: false matrix: include: - - name: Api - name: Admin + - name: Api - name: Billing - name: Events - - name: Sso - name: Identity + - name: Sso steps: - name: Setup id: setup @@ -87,16 +89,16 @@ jobs: - name: Download latest Release ${{ matrix.name }} asset if: ${{ github.event.inputs.release_type != 'Dry Run' }} - uses: bitwarden/gh-actions/download-artifacts@74f4ac01c9abe0a7331c9a5de822a558fd4a4710 + uses: bitwarden/gh-actions/download-artifacts@f096207b7a2f31723165aee6ad03e91716686e78 with: workflow: build.yml workflow_conclusion: success branch: ${{ needs.setup.outputs.branch-name }} artifacts: ${{ matrix.name }}.zip - - name: Download latest Release ${{ matrix.name }} asset + - name: Dry Run - Download latest Release ${{ matrix.name }} asset if: ${{ github.event.inputs.release_type == 'Dry Run' }} - uses: bitwarden/gh-actions/download-artifacts@74f4ac01c9abe0a7331c9a5de822a558fd4a4710 + uses: bitwarden/gh-actions/download-artifacts@f096207b7a2f31723165aee6ad03e91716686e78 with: workflow: build.yml workflow_conclusion: success @@ -173,8 +175,7 @@ jobs: release-docker: name: Build Docker images runs-on: ubuntu-22.04 - needs: - - setup + needs: setup env: _RELEASE_VERSION: ${{ needs.setup.outputs.release_version }} _BRANCH_NAME: ${{ needs.setup.outputs.branch-name }} @@ -183,40 +184,21 @@ jobs: matrix: include: - project_name: Admin - origin_docker_repo: bitwarden - project_name: Api - origin_docker_repo: bitwarden - project_name: Attachments - origin_docker_repo: bitwarden - - project_name: Events - prod_acr: true - origin_docker_repo: bitwarden - - project_name: EventsProcessor - prod_acr: true - origin_docker_repo: bitwardenprod.azurecr.io - - project_name: Icons - origin_docker_repo: bitwarden - prod_acr: true - - project_name: Identity - origin_docker_repo: bitwarden - - project_name: MsSql - origin_docker_repo: bitwarden - - project_name: Nginx - origin_docker_repo: bitwarden - - project_name: Notifications - origin_docker_repo: bitwarden - - project_name: Server - origin_docker_repo: bitwarden - - project_name: Setup - origin_docker_repo: bitwarden - - project_name: Sso - origin_docker_repo: bitwarden - - project_name: Scim - origin_docker_repo: bitwarden - project_name: Billing - origin_docker_repo: bitwardenprod.azurecr.io + - project_name: Events + - project_name: EventsProcessor + - project_name: Icons + - project_name: Identity + - project_name: MsSql - project_name: MsSqlMigratorUtility - origin_docker_repo: bitwardenprod.azurecr.io + - project_name: Nginx + - project_name: Notifications + - project_name: Scim + - project_name: Server + - project_name: Setup + - project_name: Sso steps: - name: Print environment env: @@ -239,51 +221,6 @@ jobs: echo "PROJECT_NAME: $PROJECT_NAME" echo "project_name=$PROJECT_NAME" >> $GITHUB_OUTPUT - ########## DockerHub ########## - - name: Setup DCT - id: setup-dct - if: matrix.origin_docker_repo == 'bitwarden' - uses: bitwarden/gh-actions/setup-docker-trust@74f4ac01c9abe0a7331c9a5de822a558fd4a4710 - with: - azure-creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - azure-keyvault-name: "bitwarden-ci" - - - name: Pull latest project image - if: matrix.origin_docker_repo == 'bitwarden' - env: - PROJECT_NAME: ${{ steps.setup.outputs.project_name }} - run: | - if [[ "${{ github.event.inputs.release_type }}" == "Dry Run" ]]; then - docker pull bitwarden/$PROJECT_NAME:latest - else - docker pull bitwarden/$PROJECT_NAME:$_BRANCH_NAME - fi - - - name: Tag version and latest - if: matrix.origin_docker_repo == 'bitwarden' - env: - PROJECT_NAME: ${{ steps.setup.outputs.project_name }} - run: | - if [[ "${{ github.event.inputs.release_type }}" == "Dry Run" ]]; then - docker tag bitwarden/$PROJECT_NAME:latest bitwarden/$PROJECT_NAME:dryrun - else - docker tag bitwarden/$PROJECT_NAME:$_BRANCH_NAME bitwarden/$PROJECT_NAME:$_RELEASE_VERSION - fi - - - name: Push version and latest image - if: ${{ github.event.inputs.release_type != 'Dry Run' && matrix.origin_docker_repo == 'bitwarden' }} - env: - DOCKER_CONTENT_TRUST: 1 - DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.setup-dct.outputs.dct-delegate-repo-passphrase }} - PROJECT_NAME: ${{ steps.setup.outputs.project_name }} - run: docker push bitwarden/$PROJECT_NAME:$_RELEASE_VERSION - - - name: Log out of Docker and disable Docker Notary - if: matrix.origin_docker_repo == 'bitwarden' - run: | - docker logout - echo "DOCKER_CONTENT_TRUST=0" >> $GITHUB_ENV - ########## ACR PROD ########## - name: Login to Azure - PROD Subscription uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7 @@ -291,41 +228,39 @@ jobs: creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }} - name: Login to Azure ACR - run: az acr login -n bitwardenprod + run: az acr login -n $_AZ_REGISTRY --only-show-errors - name: Pull latest project image - if: matrix.origin_docker_repo == 'bitwardenprod.azurecr.io' env: PROJECT_NAME: ${{ steps.setup.outputs.project_name }} - ORIGIN_REGISTRY: ${{ matrix.origin_docker_repo }} run: | if [[ "${{ github.event.inputs.release_type }}" == "Dry Run" ]]; then - docker pull $ORIGIN_REGISTRY/$PROJECT_NAME:dev + docker pull $_AZ_REGISTRY/$PROJECT_NAME:latest else - docker pull $ORIGIN_REGISTRY/$PROJECT_NAME:$_BRANCH_NAME + docker pull $_AZ_REGISTRY/$PROJECT_NAME:$_BRANCH_NAME fi - name: Tag version and latest env: PROJECT_NAME: ${{ steps.setup.outputs.project_name }} - REGISTRY: bitwardenprod.azurecr.io - ORIGIN_REGISTRY: ${{ matrix.origin_docker_repo }} run: | if [[ "${{ github.event.inputs.release_type }}" == "Dry Run" ]]; then - docker tag $ORIGIN_REGISTRY/$PROJECT_NAME:dev $REGISTRY/$PROJECT_NAME:dryrun + docker tag $_AZ_REGISTRY/$PROJECT_NAME:latest $_AZ_REGISTRY/$PROJECT_NAME:dryrun else - docker tag $ORIGIN_REGISTRY/$PROJECT_NAME:$_BRANCH_NAME $REGISTRY/$PROJECT_NAME:$_RELEASE_VERSION - docker tag $ORIGIN_REGISTRY/$PROJECT_NAME:$_BRANCH_NAME $REGISTRY/$PROJECT_NAME:latest + docker tag $_AZ_REGISTRY/$PROJECT_NAME:$_BRANCH_NAME $_AZ_REGISTRY/$PROJECT_NAME:$_RELEASE_VERSION + docker tag $_AZ_REGISTRY/$PROJECT_NAME:$_BRANCH_NAME $_AZ_REGISTRY/$PROJECT_NAME:latest fi - name: Push version and latest image - if: ${{ github.event.inputs.release_type != 'Dry Run' }} env: PROJECT_NAME: ${{ steps.setup.outputs.project_name }} - REGISTRY: bitwardenprod.azurecr.io run: | - docker push $REGISTRY/$PROJECT_NAME:$_RELEASE_VERSION - docker push $REGISTRY/$PROJECT_NAME:latest + if [[ "${{ github.event.inputs.release_type }}" == "Dry Run" ]]; then + docker push $_AZ_REGISTRY/$PROJECT_NAME:dryrun + else + docker push $_AZ_REGISTRY/$PROJECT_NAME:$_RELEASE_VERSION + docker push $_AZ_REGISTRY/$PROJECT_NAME:latest + fi - name: Log out of Docker run: docker logout @@ -339,7 +274,7 @@ jobs: steps: - name: Download latest Release Docker Stubs if: ${{ github.event.inputs.release_type != 'Dry Run' }} - uses: bitwarden/gh-actions/download-artifacts@74f4ac01c9abe0a7331c9a5de822a558fd4a4710 + uses: bitwarden/gh-actions/download-artifacts@f096207b7a2f31723165aee6ad03e91716686e78 with: workflow: build.yml workflow_conclusion: success @@ -350,9 +285,9 @@ jobs: docker-stub-EU-sha256.txt, swagger.json" - - name: Download latest Release Docker Stubs + - name: Dry Run - Download latest Release Docker Stubs if: ${{ github.event.inputs.release_type == 'Dry Run' }} - uses: bitwarden/gh-actions/download-artifacts@74f4ac01c9abe0a7331c9a5de822a558fd4a4710 + uses: bitwarden/gh-actions/download-artifacts@f096207b7a2f31723165aee6ad03e91716686e78 with: workflow: build.yml workflow_conclusion: success diff --git a/.github/workflows/version-bump.yml b/.github/workflows/version-bump.yml index 1c9088a92c..829298d714 100644 --- a/.github/workflows/version-bump.yml +++ b/.github/workflows/version-bump.yml @@ -23,7 +23,7 @@ jobs: - name: Retrieve secrets id: retrieve-secrets - uses: bitwarden/gh-actions/get-keyvault-secrets@74f4ac01c9abe0a7331c9a5de822a558fd4a4710 + uses: bitwarden/gh-actions/get-keyvault-secrets@f096207b7a2f31723165aee6ad03e91716686e78 with: keyvault: "bitwarden-ci" secrets: "github-gpg-private-key, github-gpg-private-key-passphrase" @@ -40,7 +40,7 @@ jobs: run: git switch -c version_bump_${{ github.event.inputs.version_number }} - name: Bump Version - Props - uses: bitwarden/gh-actions/version-bump@74f4ac01c9abe0a7331c9a5de822a558fd4a4710 + uses: bitwarden/gh-actions/version-bump@f096207b7a2f31723165aee6ad03e91716686e78 with: version: ${{ github.event.inputs.version_number }} file_path: "Directory.Build.props" diff --git a/.github/workflows/workflow-linter.yml b/.github/workflows/workflow-linter.yml index 67d01f67da..13b3eb5110 100644 --- a/.github/workflows/workflow-linter.yml +++ b/.github/workflows/workflow-linter.yml @@ -8,4 +8,4 @@ on: jobs: call-workflow: - uses: bitwarden/gh-actions/.github/workflows/workflow-linter.yml@74f4ac01c9abe0a7331c9a5de822a558fd4a4710 + uses: bitwarden/gh-actions/.github/workflows/workflow-linter.yml@f096207b7a2f31723165aee6ad03e91716686e78 diff --git a/.vscode/launch.json b/.vscode/launch.json index 0bc09f47b8..564c94e6f3 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -101,7 +101,6 @@ "stopAtEntry": false, "env": { "ASPNETCORE_ENVIRONMENT": "Development", - "WEBSITE_INSTANCE_ID": "dev", }, "sourceFileMap": { "/Views": "${workspaceFolder}/Views" @@ -124,7 +123,6 @@ "stopAtEntry": false, "env": { "ASPNETCORE_ENVIRONMENT": "Development", - "WEBSITE_INSTANCE_ID": "dev", }, "sourceFileMap": { "/Views": "${workspaceFolder}/Views" @@ -147,7 +145,6 @@ "stopAtEntry": false, "env": { "ASPNETCORE_ENVIRONMENT": "Development", - "WEBSITE_INSTANCE_ID": "dev", }, "sourceFileMap": { "/Views": "${workspaceFolder}/Views" @@ -172,7 +169,6 @@ "OS-COMMENT5": "Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser", "env": { "ASPNETCORE_ENVIRONMENT": "Development", - "WEBSITE_INSTANCE_ID": "dev", }, "sourceFileMap": { "/Views": "${workspaceFolder}/Views" @@ -195,7 +191,6 @@ "stopAtEntry": false, "env": { "ASPNETCORE_ENVIRONMENT": "Development", - "WEBSITE_INSTANCE_ID": "dev", }, "sourceFileMap": { "/Views": "${workspaceFolder}/Views" @@ -218,7 +213,6 @@ "stopAtEntry": false, "env": { "ASPNETCORE_ENVIRONMENT": "Development", - "WEBSITE_INSTANCE_ID": "dev", }, "sourceFileMap": { "/Views": "${workspaceFolder}/Views" @@ -241,7 +235,6 @@ "stopAtEntry": false, "env": { "ASPNETCORE_ENVIRONMENT": "Development", - "WEBSITE_INSTANCE_ID": "dev", }, "sourceFileMap": { "/Views": "${workspaceFolder}/Views" @@ -264,7 +257,6 @@ "stopAtEntry": false, "env": { "ASPNETCORE_ENVIRONMENT": "Development", - "WEBSITE_INSTANCE_ID": "dev", }, "sourceFileMap": { "/Views": "${workspaceFolder}/Views" @@ -289,7 +281,6 @@ "ASPNETCORE_ENVIRONMENT": "Development", "ASPNETCORE_URLS": "http://localhost:33657", "developSelfHosted": "true", - "WEBSITE_INSTANCE_ID": "dev", }, "sourceFileMap": { "/Views": "${workspaceFolder}/Views" @@ -314,7 +305,6 @@ "ASPNETCORE_ENVIRONMENT": "Development", "ASPNETCORE_URLS": "http://localhost:4001", "developSelfHosted": "true", - "WEBSITE_INSTANCE_ID": "dev", }, "sourceFileMap": { "/Views": "${workspaceFolder}/Views" @@ -341,7 +331,6 @@ "ASPNETCORE_ENVIRONMENT": "Development", "ASPNETCORE_URLS": "http://localhost:62912", "developSelfHosted": "true", - "WEBSITE_INSTANCE_ID": "dev", }, "sourceFileMap": { "/Views": "${workspaceFolder}/Views" @@ -366,7 +355,6 @@ "ASPNETCORE_ENVIRONMENT": "Development", "ASPNETCORE_URLS": "http://localhost:51822", "developSelfHosted": "true", - "WEBSITE_INSTANCE_ID": "dev", }, "sourceFileMap": { "/Views": "${workspaceFolder}/Views" @@ -391,7 +379,6 @@ "ASPNETCORE_ENVIRONMENT": "Development", "ASPNETCORE_URLS": "http://localhost:61841", "developSelfHosted": "true", - "WEBSITE_INSTANCE_ID": "dev", }, "sourceFileMap": { "/Views": "${workspaceFolder}/Views" @@ -416,7 +403,6 @@ "ASPNETCORE_ENVIRONMENT": "Development", "ASPNETCORE_URLS": "http://localhost:46274", "developSelfHosted": "true", - "WEBSITE_INSTANCE_ID": "dev", }, "sourceFileMap": { "/Views": "${workspaceFolder}/Views" diff --git a/NuGet.Config b/NuGet.Config index c3c2967a3b..ecaab16a1e 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -1,8 +1,4 @@  - - - - - + diff --git a/bitwarden_license/test/Commercial.Core.Test/packages.lock.json b/bitwarden_license/test/Commercial.Core.Test/packages.lock.json index 0882504791..09019299e5 100644 --- a/bitwarden_license/test/Commercial.Core.Test/packages.lock.json +++ b/bitwarden_license/test/Commercial.Core.Test/packages.lock.json @@ -940,15 +940,6 @@ "System.Security.Cryptography.Pkcs": "6.0.0" } }, - "Moq": { - "type": "Transitive", - "resolved": "4.17.2", - "contentHash": "HytUPJ3/uks2UgJ9hIcyXm3YxpFAR4OJzbQwTHltbKGun3lFLhEHs97hiiPj1dY8jV/kasXeihTzDxct6Zf3iQ==", - "dependencies": { - "Castle.Core": "4.4.1", - "System.Threading.Tasks.Extensions": "4.5.4" - } - }, "NETStandard.Library": { "type": "Transitive", "resolved": "1.6.1", @@ -2675,75 +2666,74 @@ "commercial.core": { "type": "Project", "dependencies": { - "Core": "2023.7.2" + "Core": "[2023.7.2, )" } }, "common": { "type": "Project", "dependencies": { - "AutoFixture.AutoNSubstitute": "4.17.0", - "AutoFixture.Xunit2": "4.17.0", - "Core": "2023.7.2", - "Kralizek.AutoFixture.Extensions.MockHttp": "1.2.0", - "Microsoft.NET.Test.Sdk": "17.1.0", - "NSubstitute": "4.3.0", - "xunit": "2.4.1" + "AutoFixture.AutoNSubstitute": "[4.17.0, )", + "AutoFixture.Xunit2": "[4.17.0, )", + "Core": "[2023.7.2, )", + "Kralizek.AutoFixture.Extensions.MockHttp": "[1.2.0, )", + "Microsoft.NET.Test.Sdk": "[17.1.0, )", + "NSubstitute": "[4.3.0, )", + "xunit": "[2.4.1, )" } }, "core": { "type": "Project", "dependencies": { - "AWSSDK.SQS": "3.7.2.47", - "AWSSDK.SimpleEmail": "3.7.0.150", - "AspNetCoreRateLimit": "4.0.2", - "AspNetCoreRateLimit.Redis": "1.0.1", - "Azure.Extensions.AspNetCore.DataProtection.Blobs": "1.3.2", - "Azure.Messaging.ServiceBus": "7.15.0", - "Azure.Storage.Blobs": "12.14.1", - "Azure.Storage.Queues": "12.12.0", - "BitPay.Light": "1.0.1907", - "Braintree": "5.12.0", - "DnsClient": "1.7.0", - "Fido2.AspNet": "3.0.1", - "Handlebars.Net": "2.1.2", - "IdentityServer4": "4.1.2", - "IdentityServer4.AccessTokenValidation": "3.0.1", - "LaunchDarkly.ServerSdk": "7.0.0", - "MailKit": "3.2.0", - "Microsoft.AspNetCore.Authentication.JwtBearer": "6.0.4", - "Microsoft.Azure.Cosmos.Table": "1.0.8", - "Microsoft.Azure.NotificationHubs": "4.1.0", - "Microsoft.Data.SqlClient": "5.0.1", - "Microsoft.Extensions.Caching.StackExchangeRedis": "6.0.6", - "Microsoft.Extensions.Configuration.EnvironmentVariables": "6.0.1", - "Microsoft.Extensions.Configuration.UserSecrets": "6.0.1", - "Microsoft.Extensions.Identity.Stores": "6.0.4", - "Newtonsoft.Json": "13.0.1", - "Otp.NET": "1.2.2", - "Quartz": "3.4.0", - "SendGrid": "9.27.0", - "Sentry.Serilog": "3.16.0", - "Serilog.AspNetCore": "5.0.0", - "Serilog.Extensions.Logging": "3.1.0", - "Serilog.Extensions.Logging.File": "2.0.0", - "Serilog.Sinks.AzureCosmosDB": "2.0.0", - "Serilog.Sinks.SyslogMessages": "2.0.6", - "Stripe.net": "40.0.0", - "YubicoDotNetClient": "1.2.0" + "AWSSDK.SQS": "[3.7.2.47, )", + "AWSSDK.SimpleEmail": "[3.7.0.150, )", + "AspNetCoreRateLimit": "[4.0.2, )", + "AspNetCoreRateLimit.Redis": "[1.0.1, )", + "Azure.Extensions.AspNetCore.DataProtection.Blobs": "[1.3.2, )", + "Azure.Messaging.ServiceBus": "[7.15.0, )", + "Azure.Storage.Blobs": "[12.14.1, )", + "Azure.Storage.Queues": "[12.12.0, )", + "BitPay.Light": "[1.0.1907, )", + "Braintree": "[5.12.0, )", + "DnsClient": "[1.7.0, )", + "Fido2.AspNet": "[3.0.1, )", + "Handlebars.Net": "[2.1.2, )", + "IdentityServer4": "[4.1.2, )", + "IdentityServer4.AccessTokenValidation": "[3.0.1, )", + "LaunchDarkly.ServerSdk": "[7.0.0, )", + "MailKit": "[3.2.0, )", + "Microsoft.AspNetCore.Authentication.JwtBearer": "[6.0.4, )", + "Microsoft.Azure.Cosmos.Table": "[1.0.8, )", + "Microsoft.Azure.NotificationHubs": "[4.1.0, )", + "Microsoft.Data.SqlClient": "[5.0.1, )", + "Microsoft.Extensions.Caching.StackExchangeRedis": "[6.0.6, )", + "Microsoft.Extensions.Configuration.EnvironmentVariables": "[6.0.1, )", + "Microsoft.Extensions.Configuration.UserSecrets": "[6.0.1, )", + "Microsoft.Extensions.Identity.Stores": "[6.0.4, )", + "Newtonsoft.Json": "[13.0.1, )", + "Otp.NET": "[1.2.2, )", + "Quartz": "[3.4.0, )", + "SendGrid": "[9.27.0, )", + "Sentry.Serilog": "[3.16.0, )", + "Serilog.AspNetCore": "[5.0.0, )", + "Serilog.Extensions.Logging": "[3.1.0, )", + "Serilog.Extensions.Logging.File": "[2.0.0, )", + "Serilog.Sinks.AzureCosmosDB": "[2.0.0, )", + "Serilog.Sinks.SyslogMessages": "[2.0.6, )", + "Stripe.net": "[40.0.0, )", + "YubicoDotNetClient": "[1.2.0, )" } }, "core.test": { "type": "Project", "dependencies": { - "AutoFixture.AutoNSubstitute": "4.17.0", - "AutoFixture.Xunit2": "4.17.0", - "Common": "2023.7.2", - "Core": "2023.7.2", - "Kralizek.AutoFixture.Extensions.MockHttp": "1.2.0", - "Microsoft.NET.Test.Sdk": "17.1.0", - "Moq": "4.17.2", - "NSubstitute": "4.3.0", - "xunit": "2.4.1" + "AutoFixture.AutoNSubstitute": "[4.17.0, )", + "AutoFixture.Xunit2": "[4.17.0, )", + "Common": "[2023.7.2, )", + "Core": "[2023.7.2, )", + "Kralizek.AutoFixture.Extensions.MockHttp": "[1.2.0, )", + "Microsoft.NET.Test.Sdk": "[17.1.0, )", + "NSubstitute": "[4.3.0, )", + "xunit": "[2.4.1, )" } } } diff --git a/global.json b/global.json index 10b65be864..527fd31d3f 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "6.0.100", + "version": "6.0.413", "rollForward": "latestFeature" } } diff --git a/src/Admin/Views/Shared/_OrganizationFormScripts.cshtml b/src/Admin/Views/Shared/_OrganizationFormScripts.cshtml index 8fd7ef10e0..7edeafb7da 100644 --- a/src/Admin/Views/Shared/_OrganizationFormScripts.cshtml +++ b/src/Admin/Views/Shared/_OrganizationFormScripts.cshtml @@ -6,7 +6,7 @@ document.getElementById('@(nameof(Model.Plan))').value = selectText; togglePlanSettings(selectEl.options[selectEl.selectedIndex].value); }); - document.getElementById('gateway-customer-link').addEventListener('click', () => { + document.getElementById('gateway-customer-link')?.addEventListener('click', () => { const gateway = document.getElementById('@(nameof(Model.Gateway))'); const customerId = document.getElementById('@(nameof(Model.GatewayCustomerId))'); if (!gateway || gateway.value === '' || !customerId || customerId.value === '') { @@ -19,7 +19,7 @@ + customerId.value, '_blank'); } }); - document.getElementById('gateway-subscription-link').addEventListener('click', () => { + document.getElementById('gateway-subscription-link')?.addEventListener('click', () => { const gateway = document.getElementById('@(nameof(Model.Gateway))'); const subId = document.getElementById('@(nameof(Model.GatewaySubscriptionId))'); if (!gateway || gateway.value === '' || !subId || subId.value === '') { @@ -34,24 +34,24 @@ }); document.getElementById('@(nameof(Model.UseSecretsManager))').addEventListener('change', (event) => { document.getElementById('organization-secrets-configuration').hidden = !event.target.checked; - + if (event.target.checked) { return; } - + document.getElementById('@(nameof(Model.SmSeats))').value = ''; document.getElementById('@(nameof(Model.MaxAutoscaleSmSeats))').value = ''; document.getElementById('@(nameof(Model.SmServiceAccounts))').value = ''; document.getElementById('@(nameof(Model.MaxAutoscaleSmServiceAccounts))').value = ''; }); })(); - + function togglePlanSettings(planType) { document.getElementById('@(nameof(Model.PlanType))').value = planType; switch(planType) { case '@((byte)Bit.Core.Enums.PlanType.TeamsMonthly)': case '@((byte)Bit.Core.Enums.PlanType.TeamsAnnually)': - // Plan + // Plan document.getElementById('@(nameof(Model.Seats))').value = '10'; document.getElementById('@(nameof(Model.MaxCollections))').value = ''; document.getElementById('@(nameof(Model.MaxStorageGb))').value = '1'; @@ -66,7 +66,7 @@ document.getElementById('@(nameof(Model.UseDirectory))').checked = true; document.getElementById('@(nameof(Model.UseEvents))').checked = true; document.getElementById('@(nameof(Model.UsersGetPremium))').checked = true; - document.getElementById('@(nameof(Model.UseCustomPermissions))').checked = false; + document.getElementById('@(nameof(Model.UseCustomPermissions))').checked = false; document.getElementById('@(nameof(Model.UseTotp))').checked = true; document.getElementById('@(nameof(Model.Use2fa))').checked = true; document.getElementById('@(nameof(Model.UseApi))').checked = true; @@ -78,7 +78,7 @@ document.getElementById('@(nameof(Model.ExpirationDate))').value = '@Model.FourteenDayExpirationDate'; document.getElementById('@(nameof(Model.SalesAssistedTrialStarted))').value = true; break; - + case '@((byte)Bit.Core.Enums.PlanType.EnterpriseMonthly)': case '@((byte)Bit.Core.Enums.PlanType.EnterpriseAnnually)': // Plan @@ -109,6 +109,6 @@ document.getElementById('@(nameof(Model.SalesAssistedTrialStarted))').value = true; break; } - + } - \ No newline at end of file + diff --git a/src/Api/Controllers/CollectionsController.cs b/src/Api/Controllers/CollectionsController.cs index 4fc2ffc201..879ec077d2 100644 --- a/src/Api/Controllers/CollectionsController.cs +++ b/src/Api/Controllers/CollectionsController.cs @@ -112,7 +112,7 @@ public class CollectionsController : Controller [HttpGet("")] public async Task> Get(Guid orgId) { - IEnumerable orgCollections = await _collectionService.GetOrganizationCollections(orgId); + IEnumerable orgCollections = await _collectionService.GetOrganizationCollectionsAsync(orgId); var responses = orgCollections.Select(c => new CollectionResponseModel(c)); return new ListResponseModel(responses); @@ -209,7 +209,7 @@ public class CollectionsController : Controller throw new NotFoundException(); } - var userCollections = await _collectionService.GetOrganizationCollections(orgId); + var userCollections = await _collectionService.GetOrganizationCollectionsAsync(orgId); var filteredCollections = userCollections.Where(c => collectionIds.Contains(c.Id) && c.OrganizationId == orgId); if (!filteredCollections.Any()) diff --git a/src/Api/Controllers/OrganizationExportController.cs b/src/Api/Controllers/OrganizationExportController.cs index 1261b938c2..be5aa0e14a 100644 --- a/src/Api/Controllers/OrganizationExportController.cs +++ b/src/Api/Controllers/OrganizationExportController.cs @@ -40,7 +40,7 @@ public class OrganizationExportController : Controller { var userId = _userService.GetProperUserId(User).Value; - IEnumerable orgCollections = await _collectionService.GetOrganizationCollections(organizationId); + IEnumerable orgCollections = await _collectionService.GetOrganizationCollectionsAsync(organizationId); (IEnumerable orgCiphers, Dictionary> collectionCiphersGroupDict) = await _cipherService.GetOrganizationCiphers(userId, organizationId); if (_currentContext.ClientVersion == null || _currentContext.ClientVersion >= new Version("2023.1.0")) diff --git a/src/Api/SecretsManager/Models/Request/ProjectCreateRequestModel.cs b/src/Api/SecretsManager/Models/Request/ProjectCreateRequestModel.cs index 65c4d244d2..3014ecdf82 100644 --- a/src/Api/SecretsManager/Models/Request/ProjectCreateRequestModel.cs +++ b/src/Api/SecretsManager/Models/Request/ProjectCreateRequestModel.cs @@ -8,6 +8,7 @@ public class ProjectCreateRequestModel { [Required] [EncryptedString] + [EncryptedStringLength(1000)] public string Name { get; set; } public Project ToProject(Guid organizationId) diff --git a/src/Api/SecretsManager/Models/Request/ProjectUpdateRequestModel.cs b/src/Api/SecretsManager/Models/Request/ProjectUpdateRequestModel.cs index 4490e02c4e..176b6cc598 100644 --- a/src/Api/SecretsManager/Models/Request/ProjectUpdateRequestModel.cs +++ b/src/Api/SecretsManager/Models/Request/ProjectUpdateRequestModel.cs @@ -8,6 +8,7 @@ public class ProjectUpdateRequestModel { [Required] [EncryptedString] + [EncryptedStringLength(1000)] public string Name { get; set; } public Project ToProject(Guid id) diff --git a/src/Api/SecretsManager/Models/Request/SecretCreateRequestModel.cs b/src/Api/SecretsManager/Models/Request/SecretCreateRequestModel.cs index c243d6e465..9d6f8c9aa1 100644 --- a/src/Api/SecretsManager/Models/Request/SecretCreateRequestModel.cs +++ b/src/Api/SecretsManager/Models/Request/SecretCreateRequestModel.cs @@ -8,14 +8,17 @@ public class SecretCreateRequestModel : IValidatableObject { [Required] [EncryptedString] + [EncryptedStringLength(1000)] public string Key { get; set; } [Required] [EncryptedString] + [EncryptedStringLength(5000)] public string Value { get; set; } [Required] [EncryptedString] + [EncryptedStringLength(10000)] public string Note { get; set; } public Guid[] ProjectIds { get; set; } diff --git a/src/Api/SecretsManager/Models/Request/SecretUpdateRequestModel.cs b/src/Api/SecretsManager/Models/Request/SecretUpdateRequestModel.cs index 6416849d24..d5cd320fff 100644 --- a/src/Api/SecretsManager/Models/Request/SecretUpdateRequestModel.cs +++ b/src/Api/SecretsManager/Models/Request/SecretUpdateRequestModel.cs @@ -8,14 +8,17 @@ public class SecretUpdateRequestModel : IValidatableObject { [Required] [EncryptedString] + [EncryptedStringLength(1000)] public string Key { get; set; } [Required] [EncryptedString] + [EncryptedStringLength(5000)] public string Value { get; set; } [Required] [EncryptedString] + [EncryptedStringLength(10000)] public string Note { get; set; } public Guid[] ProjectIds { get; set; } diff --git a/src/Api/SecretsManager/Models/Request/SerivceAccountUpdateRequestModel.cs b/src/Api/SecretsManager/Models/Request/SerivceAccountUpdateRequestModel.cs index 2a1885142e..017749725f 100644 --- a/src/Api/SecretsManager/Models/Request/SerivceAccountUpdateRequestModel.cs +++ b/src/Api/SecretsManager/Models/Request/SerivceAccountUpdateRequestModel.cs @@ -8,6 +8,7 @@ public class ServiceAccountUpdateRequestModel { [Required] [EncryptedString] + [EncryptedStringLength(1000)] public string Name { get; set; } public ServiceAccount ToServiceAccount(Guid id) diff --git a/src/Api/SecretsManager/Models/Request/ServiceAccountCreateRequestModel.cs b/src/Api/SecretsManager/Models/Request/ServiceAccountCreateRequestModel.cs index 60ab75b682..6771669209 100644 --- a/src/Api/SecretsManager/Models/Request/ServiceAccountCreateRequestModel.cs +++ b/src/Api/SecretsManager/Models/Request/ServiceAccountCreateRequestModel.cs @@ -8,6 +8,7 @@ public class ServiceAccountCreateRequestModel { [Required] [EncryptedString] + [EncryptedStringLength(1000)] public string Name { get; set; } public ServiceAccount ToServiceAccount(Guid organizationId) diff --git a/src/Core/Context/CurrentContext.cs b/src/Core/Context/CurrentContext.cs index 512ab44310..627e4f5845 100644 --- a/src/Core/Context/CurrentContext.cs +++ b/src/Core/Context/CurrentContext.cs @@ -341,7 +341,7 @@ public class CurrentContext : ICurrentContext public async Task ViewAllCollections(Guid orgId) { - return await CreateNewCollections(orgId) || await EditAnyCollection(orgId) || await DeleteAnyCollection(orgId); + return await EditAnyCollection(orgId) || await DeleteAnyCollection(orgId); } public async Task EditAssignedCollections(Guid orgId) diff --git a/src/Core/Services/ICollectionService.cs b/src/Core/Services/ICollectionService.cs index 5da8d639e5..931993dacb 100644 --- a/src/Core/Services/ICollectionService.cs +++ b/src/Core/Services/ICollectionService.cs @@ -7,5 +7,5 @@ public interface ICollectionService { Task SaveAsync(Collection collection, IEnumerable groups = null, IEnumerable users = null, Guid? assignUserId = null); Task DeleteUserAsync(Collection collection, Guid organizationUserId); - Task> GetOrganizationCollections(Guid organizationId); + Task> GetOrganizationCollectionsAsync(Guid organizationId); } diff --git a/src/Core/Services/Implementations/CollectionService.cs b/src/Core/Services/Implementations/CollectionService.cs index 16698a77aa..006c8c5cfc 100644 --- a/src/Core/Services/Implementations/CollectionService.cs +++ b/src/Core/Services/Implementations/CollectionService.cs @@ -96,9 +96,9 @@ public class CollectionService : ICollectionService await _eventService.LogOrganizationUserEventAsync(orgUser, Enums.EventType.OrganizationUser_Updated); } - public async Task> GetOrganizationCollections(Guid organizationId) + public async Task> GetOrganizationCollectionsAsync(Guid organizationId) { - if (!await _currentContext.ViewAllCollections(organizationId) && !await _currentContext.ManageUsers(organizationId) && !await _currentContext.ManageGroups(organizationId) && !await _currentContext.AccessImportExport(organizationId)) + if (!await _currentContext.ViewAssignedCollections(organizationId) && !await _currentContext.ManageUsers(organizationId) && !await _currentContext.ManageGroups(organizationId) && !await _currentContext.AccessImportExport(organizationId)) { throw new NotFoundException(); } diff --git a/src/Icons/Controllers/IconsController.cs b/src/Icons/Controllers/IconsController.cs index ad9b6cfd4f..871219b366 100644 --- a/src/Icons/Controllers/IconsController.cs +++ b/src/Icons/Controllers/IconsController.cs @@ -81,7 +81,7 @@ public class IconsController : Controller } else { - icon = result.Icon; + icon = result; } // Only cache not found and smaller images (<= 50kb) diff --git a/src/Icons/Icons.csproj b/src/Icons/Icons.csproj index 5ecd139466..ce698eb729 100644 --- a/src/Icons/Icons.csproj +++ b/src/Icons/Icons.csproj @@ -7,7 +7,7 @@ - + diff --git a/src/Icons/Models/DomainIcons.cs b/src/Icons/Models/DomainIcons.cs new file mode 100644 index 0000000000..2ad2df29c5 --- /dev/null +++ b/src/Icons/Models/DomainIcons.cs @@ -0,0 +1,100 @@ +#nullable enable + +using System.Collections; +using AngleSharp.Html.Parser; +using Bit.Icons.Extensions; +using Bit.Icons.Services; + +namespace Bit.Icons.Models; + +public class DomainIcons : IEnumerable +{ + private readonly ILogger _logger; + private readonly IHttpClientFactory _httpClientFactory; + private readonly IUriService _uriService; + private readonly List _icons = new(); + + public string Domain { get; } + public Icon this[int i] + { + get + { + return _icons[i]; + } + } + public IEnumerator GetEnumerator() => ((IEnumerable)_icons).GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_icons).GetEnumerator(); + + private DomainIcons(string domain, ILogger logger, IHttpClientFactory httpClientFactory, IUriService uriService) + { + _logger = logger; + _httpClientFactory = httpClientFactory; + _uriService = uriService; + Domain = domain; + } + + public static async Task FetchAsync(string domain, ILogger logger, IHttpClientFactory httpClientFactory, IHtmlParser parser, IUriService uriService) + { + var pageIcons = new DomainIcons(domain, logger, httpClientFactory, uriService); + await pageIcons.FetchIconsAsync(parser); + return pageIcons; + } + + + private async Task FetchIconsAsync(IHtmlParser parser) + { + if (!Uri.TryCreate($"https://{Domain}", UriKind.Absolute, out var uri)) + { + _logger.LogWarning("Bad domain: {domain}.", Domain); + return; + } + + var host = uri.Host; + + // first try https + using (var response = await IconHttpRequest.FetchAsync(uri, _logger, _httpClientFactory, _uriService)) + { + if (response.IsSuccessStatusCode) + { + _icons.AddRange(await response.RetrieveIconsAsync(uri, Domain, parser)); + return; + } + } + + // then try http + uri = uri.ChangeScheme("http"); + using (var response = await IconHttpRequest.FetchAsync(uri, _logger, _httpClientFactory, _uriService)) + { + if (response.IsSuccessStatusCode) + { + _icons.AddRange(await response.RetrieveIconsAsync(uri, Domain, parser)); + return; + } + } + + var dotCount = Domain.Count(c => c == '.'); + + // Then try base domain + if (dotCount > 1 && DomainName.TryParseBaseDomain(Domain, out var baseDomain) && + Uri.TryCreate($"https://{baseDomain}", UriKind.Absolute, out uri)) + { + using var response = await IconHttpRequest.FetchAsync(uri, _logger, _httpClientFactory, _uriService); + if (response.IsSuccessStatusCode) + { + _icons.AddRange(await response.RetrieveIconsAsync(uri, Domain, parser)); + return; + } + } + + // Then try www + if (dotCount < 2 && Uri.TryCreate($"https://www.{host}", UriKind.Absolute, out uri)) + { + using var response = await IconHttpRequest.FetchAsync(uri, _logger, _httpClientFactory, _uriService); + if (response.IsSuccessStatusCode) + { + _icons.AddRange(await response.RetrieveIconsAsync(uri, Domain, parser)); + return; + } + } + } +} diff --git a/src/Icons/Models/IconHttpRequest.cs b/src/Icons/Models/IconHttpRequest.cs new file mode 100644 index 0000000000..746f39be9c --- /dev/null +++ b/src/Icons/Models/IconHttpRequest.cs @@ -0,0 +1,110 @@ +#nullable enable + +using System.Net; +using Bit.Icons.Extensions; +using Bit.Icons.Services; + +namespace Bit.Icons.Models; + +public class IconHttpRequest +{ + private const int _maxRedirects = 2; + + private static readonly HttpStatusCode[] _redirectStatusCodes = new HttpStatusCode[] { HttpStatusCode.Redirect, HttpStatusCode.MovedPermanently, HttpStatusCode.RedirectKeepVerb, HttpStatusCode.SeeOther }; + + private readonly ILogger _logger; + private readonly HttpClient _httpClient; + private readonly IHttpClientFactory _httpClientFactory; + private readonly IUriService _uriService; + private readonly int _redirectsCount; + private readonly Uri _uri; + private static HttpResponseMessage NotFound => new(HttpStatusCode.NotFound); + + private IconHttpRequest(Uri uri, ILogger logger, IHttpClientFactory httpClientFactory, IUriService uriService, int redirectsCount) + { + _logger = logger; + _httpClientFactory = httpClientFactory; + _httpClient = _httpClientFactory.CreateClient("Icons"); + _uriService = uriService; + _redirectsCount = redirectsCount; + _uri = uri; + } + + public static async Task FetchAsync(Uri uri, ILogger logger, IHttpClientFactory httpClientFactory, IUriService uriService) + { + var pageIcons = new IconHttpRequest(uri, logger, httpClientFactory, uriService, 0); + var httpResponse = await pageIcons.FetchAsync(); + return new IconHttpResponse(httpResponse, logger, httpClientFactory, uriService); + } + + private async Task FetchAsync() + { + if (!_uriService.TryGetUri(_uri, out var iconUri) || !iconUri!.IsValid) + { + return NotFound; + } + + var response = await GetAsync(iconUri); + + if (response.IsSuccessStatusCode) + { + return response; + } + + using var responseForRedirect = response; + return await FollowRedirectsAsync(responseForRedirect, iconUri); + } + + + private async Task GetAsync(IconUri iconUri) + { + using var message = new HttpRequestMessage(); + message.RequestUri = iconUri.InnerUri; + message.Headers.Host = iconUri.Host; + message.Method = HttpMethod.Get; + + try + { + return await _httpClient.SendAsync(message); + } + catch + { + return NotFound; + } + } + + private async Task FollowRedirectsAsync(HttpResponseMessage response, IconUri originalIconUri) + { + if (_redirectsCount >= _maxRedirects || response.Headers.Location == null || + !_redirectStatusCodes.Contains(response.StatusCode)) + { + return NotFound; + } + + using var responseForRedirect = response; + var redirectUri = DetermineRedirectUri(responseForRedirect.Headers.Location, originalIconUri); + + return await new IconHttpRequest(redirectUri, _logger, _httpClientFactory, _uriService, _redirectsCount + 1).FetchAsync(); + } + + private static Uri DetermineRedirectUri(Uri responseUri, IconUri originalIconUri) + { + if (responseUri.IsAbsoluteUri) + { + if (!responseUri.IsHypertext()) + { + return responseUri.ChangeScheme("https"); + } + return responseUri; + } + else + { + return new UriBuilder + { + Scheme = originalIconUri.Scheme, + Host = originalIconUri.Host, + Path = responseUri.ToString() + }.Uri; + } + } +} diff --git a/src/Icons/Models/IconHttpResponse.cs b/src/Icons/Models/IconHttpResponse.cs new file mode 100644 index 0000000000..a897069822 --- /dev/null +++ b/src/Icons/Models/IconHttpResponse.cs @@ -0,0 +1,72 @@ +#nullable enable + +using System.Net; +using AngleSharp.Html.Parser; +using Bit.Icons.Services; + +namespace Bit.Icons.Models; + +public class IconHttpResponse : IDisposable +{ + private const int _maxIconLinksProcessed = 200; + private const int _maxRetrievedIcons = 10; + + private readonly HttpResponseMessage _response; + private readonly ILogger _logger; + private readonly IHttpClientFactory _httpClientFactory; + private readonly IUriService _uriService; + + public HttpStatusCode StatusCode => _response.StatusCode; + public bool IsSuccessStatusCode => _response.IsSuccessStatusCode; + public string? ContentType => _response.Content.Headers.ContentType?.MediaType; + public HttpContent Content => _response.Content; + + public IconHttpResponse(HttpResponseMessage response, ILogger logger, IHttpClientFactory httpClientFactory, IUriService uriService) + { + _response = response; + _logger = logger; + _httpClientFactory = httpClientFactory; + _uriService = uriService; + } + + public async Task> RetrieveIconsAsync(Uri requestUri, string domain, IHtmlParser parser) + { + using var htmlStream = await _response.Content.ReadAsStreamAsync(); + var head = await parser.ParseHeadAsync(htmlStream); + + if (head == null) + { + _logger.LogWarning("No DocumentElement for {domain}.", domain); + return Array.Empty(); + } + + // Make sure uri uses domain name, not ip + var uri = _response.RequestMessage?.RequestUri; + if (uri == null || IPAddress.TryParse(_response.RequestMessage!.RequestUri!.Host, out var _)) + { + uri = requestUri; + } + + var baseUrl = head.QuerySelector("base[href]")?.Attributes["href"]?.Value; + if (string.IsNullOrWhiteSpace(baseUrl)) + { + baseUrl = "/"; + } + + var links = head.QuerySelectorAll("link[href]") + ?.Take(_maxIconLinksProcessed) + .Select(l => new IconLink(l, uri, baseUrl)) + .Where(l => l.IsUsable()) + .OrderBy(l => l.Priority) + .Take(_maxRetrievedIcons) + .ToArray() ?? Array.Empty(); + var results = await Task.WhenAll(links.Select(l => l.FetchAsync(_logger, _httpClientFactory, _uriService))); + return results.Where(r => r != null).Select(r => r!); + } + + + public void Dispose() + { + _response.Dispose(); + } +} diff --git a/src/Icons/Models/IconLink.cs b/src/Icons/Models/IconLink.cs new file mode 100644 index 0000000000..8c09058bb2 --- /dev/null +++ b/src/Icons/Models/IconLink.cs @@ -0,0 +1,220 @@ +#nullable enable + +using System.Text; +using AngleSharp.Dom; +using Bit.Icons.Extensions; +using Bit.Icons.Services; + +namespace Bit.Icons.Models; + +public class IconLink +{ + private static readonly HashSet _iconRels = new(StringComparer.InvariantCultureIgnoreCase) { "icon", "apple-touch-icon", "shortcut icon" }; + private static readonly HashSet _blocklistedRels = new(StringComparer.InvariantCultureIgnoreCase) { "preload", "image_src", "preconnect", "canonical", "alternate", "stylesheet" }; + private static readonly HashSet _iconExtensions = new(StringComparer.InvariantCultureIgnoreCase) { ".ico", ".png", ".jpg", ".jpeg" }; + private const string _pngMediaType = "image/png"; + private static readonly byte[] _pngHeader = new byte[] { 137, 80, 78, 71 }; + private static readonly byte[] _webpHeader = Encoding.UTF8.GetBytes("RIFF"); + + private const string _icoMediaType = "image/x-icon"; + private const string _icoAltMediaType = "image/vnd.microsoft.icon"; + private static readonly byte[] _icoHeader = new byte[] { 00, 00, 01, 00 }; + + private const string _jpegMediaType = "image/jpeg"; + private static readonly byte[] _jpegHeader = new byte[] { 255, 216, 255 }; + + private const string _svgXmlMediaType = "image/svg+xml"; + + private static readonly HashSet _allowedMediaTypes = new(StringComparer.InvariantCultureIgnoreCase) + { + _pngMediaType, + _icoMediaType, + _icoAltMediaType, + _jpegMediaType, + _svgXmlMediaType, + }; + + private bool _useUriDirectly = false; + private bool _validated = false; + private int? _width; + private int? _height; + + public IAttr? Href { get; } + public IAttr? Rel { get; } + public IAttr? Type { get; } + public IAttr? Sizes { get; } + public Uri ParentUri { get; } + public string BaseUrlPath { get; } + public int Priority + { + get + { + if (_width == null || _width != _height) + { + return 200; + } + + return _width switch + { + 32 => 1, + 64 => 2, + >= 24 and <= 128 => 3, + 16 => 4, + _ => 100, + }; + } + } + + public IconLink(Uri parentPage) + { + _useUriDirectly = true; + _validated = true; + ParentUri = parentPage; + BaseUrlPath = parentPage.PathAndQuery; + } + + public IconLink(IElement element, Uri parentPage, string baseUrlPath) + { + Href = element.Attributes["href"]; + ParentUri = parentPage; + BaseUrlPath = baseUrlPath; + + Rel = element.Attributes["rel"]; + Type = element.Attributes["type"]; + Sizes = element.Attributes["sizes"]; + + if (!string.IsNullOrWhiteSpace(Sizes?.Value)) + { + var sizeParts = Sizes.Value.Split('x'); + if (sizeParts.Length == 2 && int.TryParse(sizeParts[0].Trim(), out var width) && + int.TryParse(sizeParts[1].Trim(), out var height)) + { + _width = width; + _height = height; + } + } + } + + public bool IsUsable() + { + if (string.IsNullOrWhiteSpace(Href?.Value)) + { + return false; + } + + if (Rel != null && _iconRels.Contains(Rel.Value)) + { + _validated = true; + } + if (Rel == null || !_blocklistedRels.Contains(Rel.Value)) + { + try + { + var extension = Path.GetExtension(Href.Value); + if (_iconExtensions.Contains(extension)) + { + _validated = true; + } + } + catch (ArgumentException) { } + } + return _validated; + } + + /// + /// Fetches the icon from the Href. Will always fail unless first validated with IsUsable(). + /// + public async Task FetchAsync(ILogger logger, IHttpClientFactory httpClientFactory, IUriService uriService) + { + if (!_validated) + { + return null; + } + + var uri = BuildUri(); + if (uri == null) + { + return null; + } + + using var response = await IconHttpRequest.FetchAsync(uri, logger, httpClientFactory, uriService); + if (!response.IsSuccessStatusCode) + { + return null; + } + + var format = response.Content.Headers.ContentType?.MediaType; + var bytes = await response.Content.ReadAsByteArrayAsync(); + response.Content.Dispose(); + if (format == null || !_allowedMediaTypes.Contains(format)) + { + format = DetermineImageFormatFromFile(bytes); + } + + if (format == null || !_allowedMediaTypes.Contains(format)) + { + return null; + } + + return new Icon { Image = bytes, Format = format }; + } + + private Uri? BuildUri() + { + if (_useUriDirectly) + { + return ParentUri; + } + + if (Href == null) + { + return null; + } + + if (Href.Value.StartsWith("//") && Uri.TryCreate($"{ParentUri.Scheme}://{Href.Value[2..]}", UriKind.Absolute, out var uri)) + { + return uri; + } + + if (Uri.TryCreate(Href.Value, UriKind.Relative, out uri)) + { + return new UriBuilder() + { + Scheme = ParentUri.Scheme, + Host = ParentUri.Host, + }.Uri.ConcatPath(BaseUrlPath, uri.OriginalString); + } + + if (Uri.TryCreate(Href.Value, UriKind.Absolute, out uri)) + { + return uri; + } + + return null; + } + + private static bool HeaderMatch(byte[] imageBytes, byte[] header) + { + return imageBytes.Length >= header.Length && header.SequenceEqual(imageBytes.Take(header.Length)); + } + + private static string DetermineImageFormatFromFile(byte[] imageBytes) + { + if (HeaderMatch(imageBytes, _icoHeader)) + { + return _icoMediaType; + } + else if (HeaderMatch(imageBytes, _pngHeader) || HeaderMatch(imageBytes, _webpHeader)) + { + return _pngMediaType; + } + else if (HeaderMatch(imageBytes, _jpegHeader)) + { + return _jpegMediaType; + } + else + { + return string.Empty; + } + } +} diff --git a/src/Icons/Models/IconResult.cs b/src/Icons/Models/IconResult.cs deleted file mode 100644 index ca1e6929ed..0000000000 --- a/src/Icons/Models/IconResult.cs +++ /dev/null @@ -1,65 +0,0 @@ -namespace Bit.Icons.Models; - -public class IconResult -{ - public IconResult(string href, string sizes) - { - Path = href; - if (!string.IsNullOrWhiteSpace(sizes)) - { - var sizeParts = sizes.Split('x'); - if (sizeParts.Length == 2 && int.TryParse(sizeParts[0].Trim(), out var width) && - int.TryParse(sizeParts[1].Trim(), out var height)) - { - DefinedWidth = width; - DefinedHeight = height; - - if (width == height) - { - if (width == 32) - { - Priority = 1; - } - else if (width == 64) - { - Priority = 2; - } - else if (width >= 24 && width <= 128) - { - Priority = 3; - } - else if (width == 16) - { - Priority = 4; - } - else - { - Priority = 100; - } - } - } - } - - if (Priority == 0) - { - Priority = 200; - } - } - - public IconResult(Uri uri, byte[] bytes, string format) - { - Path = uri.ToString(); - Icon = new Icon - { - Image = bytes, - Format = format - }; - Priority = 10; - } - - public string Path { get; set; } - public int? DefinedWidth { get; set; } - public int? DefinedHeight { get; set; } - public Icon Icon { get; set; } - public int Priority { get; set; } -} diff --git a/src/Icons/Models/IconUri.cs b/src/Icons/Models/IconUri.cs new file mode 100644 index 0000000000..143bc26f72 --- /dev/null +++ b/src/Icons/Models/IconUri.cs @@ -0,0 +1,52 @@ +#nullable enable + +using System.Net; +using Bit.Icons.Extensions; + +namespace Bit.Icons.Models; + +public class IconUri +{ + private readonly IPAddress _ip; + public string Host { get; } + public Uri InnerUri { get; } + public string Scheme => InnerUri.Scheme; + + public bool IsValid + { + get + { + // Prevent direct access to any ip + if (IPAddress.TryParse(Host, out _)) + { + return false; + } + + // Prevent non-http(s) and non-default ports + if ((InnerUri.Scheme != "http" && InnerUri.Scheme != "https") || !InnerUri.IsDefaultPort) + { + return false; + } + + // Prevent local hosts (localhost, bobs-pc, etc) and IP addresses + if (!Host.Contains('.') || _ip.IsInternal()) + { + return false; + } + + return true; + } + } + + /// + /// Represents an ip-validated Uri for use in grabbing an icon. + /// + /// + /// + public IconUri(Uri uri, IPAddress ip) + { + _ip = ip; + InnerUri = uri.ChangeHost(_ip.ToString()); + Host = uri.Host; + } +} diff --git a/src/Icons/Services/IIconFetchingService.cs b/src/Icons/Services/IIconFetchingService.cs index ff6704291f..365bff78f6 100644 --- a/src/Icons/Services/IIconFetchingService.cs +++ b/src/Icons/Services/IIconFetchingService.cs @@ -1,8 +1,10 @@ -using Bit.Icons.Models; +#nullable enable + +using Bit.Icons.Models; namespace Bit.Icons.Services; public interface IIconFetchingService { - Task GetIconAsync(string domain); + Task GetIconAsync(string domain); } diff --git a/src/Icons/Services/IUriService.cs b/src/Icons/Services/IUriService.cs new file mode 100644 index 0000000000..3927d15bfd --- /dev/null +++ b/src/Icons/Services/IUriService.cs @@ -0,0 +1,12 @@ +#nullable enable + +using Bit.Icons.Models; + +namespace Bit.Icons.Services; + +public interface IUriService +{ + bool TryGetUri(string stringUri, out IconUri? iconUri); + bool TryGetUri(Uri uri, out IconUri? iconUri); + bool TryGetRedirect(HttpResponseMessage response, IconUri originalUri, out IconUri? iconUri); +} diff --git a/src/Icons/Services/IconFetchingService.cs b/src/Icons/Services/IconFetchingService.cs index 166d5a0aa7..b2b8d016a5 100644 --- a/src/Icons/Services/IconFetchingService.cs +++ b/src/Icons/Services/IconFetchingService.cs @@ -1,449 +1,47 @@ -using System.Net; -using System.Text; +#nullable enable + using AngleSharp.Html.Parser; +using Bit.Icons.Extensions; using Bit.Icons.Models; namespace Bit.Icons.Services; public class IconFetchingService : IIconFetchingService { - private readonly HashSet _iconRels = - new HashSet { "icon", "apple-touch-icon", "shortcut icon" }; - private readonly HashSet _blacklistedRels = - new HashSet { "preload", "image_src", "preconnect", "canonical", "alternate", "stylesheet" }; - private readonly HashSet _iconExtensions = - new HashSet { ".ico", ".png", ".jpg", ".jpeg" }; - - private readonly string _pngMediaType = "image/png"; - private readonly byte[] _pngHeader = new byte[] { 137, 80, 78, 71 }; - private readonly byte[] _webpHeader = Encoding.UTF8.GetBytes("RIFF"); - - private readonly string _icoMediaType = "image/x-icon"; - private readonly string _icoAltMediaType = "image/vnd.microsoft.icon"; - private readonly byte[] _icoHeader = new byte[] { 00, 00, 01, 00 }; - - private readonly string _jpegMediaType = "image/jpeg"; - private readonly byte[] _jpegHeader = new byte[] { 255, 216, 255 }; - - private readonly HashSet _allowedMediaTypes; - private readonly HttpClient _httpClient; + private readonly IHttpClientFactory _httpClientFactory; private readonly ILogger _logger; + private readonly IHtmlParser _parser; + private readonly IUriService _uriService; - public IconFetchingService(ILogger logger) + public IconFetchingService(ILogger logger, IHttpClientFactory httpClientFactory, IHtmlParser parser, IUriService uriService) { _logger = logger; - _allowedMediaTypes = new HashSet + _httpClientFactory = httpClientFactory; + _parser = parser; + _uriService = uriService; + } + + public async Task GetIconAsync(string domain) + { + var domainIcons = await DomainIcons.FetchAsync(domain, _logger, _httpClientFactory, _parser, _uriService); + var result = domainIcons.Where(result => result != null).FirstOrDefault(); + return result ?? await GetFaviconAsync(domain); + } + + private async Task GetFaviconAsync(string domain) + { + // Fall back to favicon + var faviconUriBuilder = new UriBuilder { - _pngMediaType, - _icoMediaType, - _icoAltMediaType, - _jpegMediaType + Scheme = "https", + Host = domain, + Path = "/favicon.ico" }; - _httpClient = new HttpClient(new HttpClientHandler + if (faviconUriBuilder.TryBuild(out var faviconUri)) { - AllowAutoRedirect = false, - AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate, - }); - _httpClient.Timeout = TimeSpan.FromSeconds(20); - _httpClient.MaxResponseContentBufferSize = 5000000; // 5 MB - } - - public async Task GetIconAsync(string domain) - { - if (IPAddress.TryParse(domain, out _)) - { - _logger.LogWarning("IP address: {0}.", domain); - return null; + return await new IconLink(faviconUri!).FetchAsync(_logger, _httpClientFactory, _uriService); } - - if (!Uri.TryCreate($"https://{domain}", UriKind.Absolute, out var parsedHttpsUri)) - { - _logger.LogWarning("Bad domain: {0}.", domain); - return null; - } - - var uri = parsedHttpsUri; - var response = await GetAndFollowAsync(uri, 2); - if ((response == null || !response.IsSuccessStatusCode) && - Uri.TryCreate($"http://{parsedHttpsUri.Host}", UriKind.Absolute, out var parsedHttpUri)) - { - Cleanup(response); - uri = parsedHttpUri; - response = await GetAndFollowAsync(uri, 2); - - if (response == null || !response.IsSuccessStatusCode) - { - var dotCount = domain.Count(c => c == '.'); - if (dotCount > 1 && DomainName.TryParseBaseDomain(domain, out var baseDomain) && - Uri.TryCreate($"https://{baseDomain}", UriKind.Absolute, out var parsedBaseUri)) - { - Cleanup(response); - uri = parsedBaseUri; - response = await GetAndFollowAsync(uri, 2); - } - else if (dotCount < 2 && - Uri.TryCreate($"https://www.{parsedHttpsUri.Host}", UriKind.Absolute, out var parsedWwwUri)) - { - Cleanup(response); - uri = parsedWwwUri; - response = await GetAndFollowAsync(uri, 2); - } - } - } - - if (response?.Content == null || !response.IsSuccessStatusCode) - { - _logger.LogWarning("Couldn't load a website for {0}: {1}.", domain, - response?.StatusCode.ToString() ?? "null"); - Cleanup(response); - return null; - } - - var parser = new HtmlParser(); - using (response) - using (var htmlStream = await response.Content.ReadAsStreamAsync()) - using (var document = await parser.ParseDocumentAsync(htmlStream)) - { - uri = response.RequestMessage.RequestUri; - if (document.DocumentElement == null) - { - _logger.LogWarning("No DocumentElement for {0}.", domain); - return null; - } - - var baseUrl = "/"; - var baseUrlNode = document.QuerySelector("head base[href]"); - if (baseUrlNode != null) - { - var hrefAttr = baseUrlNode.Attributes["href"]; - if (!string.IsNullOrWhiteSpace(hrefAttr?.Value)) - { - baseUrl = hrefAttr.Value; - } - - baseUrlNode = null; - hrefAttr = null; - } - - var icons = new List(); - var links = document.QuerySelectorAll("head link[href]"); - if (links != null) - { - foreach (var link in links.Take(200)) - { - var hrefAttr = link.Attributes["href"]; - if (string.IsNullOrWhiteSpace(hrefAttr?.Value)) - { - continue; - } - - var relAttr = link.Attributes["rel"]; - var sizesAttr = link.Attributes["sizes"]; - if (relAttr != null && _iconRels.Contains(relAttr.Value.ToLower())) - { - icons.Add(new IconResult(hrefAttr.Value, sizesAttr?.Value)); - } - else if (relAttr == null || !_blacklistedRels.Contains(relAttr.Value.ToLower())) - { - try - { - var extension = Path.GetExtension(hrefAttr.Value); - if (_iconExtensions.Contains(extension.ToLower())) - { - icons.Add(new IconResult(hrefAttr.Value, sizesAttr?.Value)); - } - } - catch (ArgumentException) { } - } - - sizesAttr = null; - relAttr = null; - hrefAttr = null; - } - - links = null; - } - - var iconResultTasks = new List(); - foreach (var icon in icons.OrderBy(i => i.Priority).Take(10)) - { - Uri iconUri = null; - if (icon.Path.StartsWith("//") && Uri.TryCreate($"{GetScheme(uri)}://{icon.Path.Substring(2)}", - UriKind.Absolute, out var slashUri)) - { - iconUri = slashUri; - } - else if (Uri.TryCreate(icon.Path, UriKind.Relative, out var relUri)) - { - iconUri = ResolveUri($"{GetScheme(uri)}://{uri.Host}", baseUrl, relUri.OriginalString); - } - else if (Uri.TryCreate(icon.Path, UriKind.Absolute, out var absUri)) - { - iconUri = absUri; - } - - if (iconUri != null) - { - var task = GetIconAsync(iconUri).ContinueWith(async (r) => - { - var result = await r; - if (result != null) - { - icon.Path = iconUri.ToString(); - icon.Icon = result.Icon; - } - }); - iconResultTasks.Add(task); - } - } - - await Task.WhenAll(iconResultTasks); - if (!icons.Any(i => i.Icon != null)) - { - var faviconUri = ResolveUri($"{GetScheme(uri)}://{uri.Host}", "favicon.ico"); - var result = await GetIconAsync(faviconUri); - if (result != null) - { - icons.Add(result); - } - else - { - _logger.LogWarning("No favicon.ico found for {0}.", uri.Host); - return null; - } - } - - return icons.Where(i => i.Icon != null).OrderBy(i => i.Priority).First(); - } - } - - private async Task GetIconAsync(Uri uri) - { - using (var response = await GetAndFollowAsync(uri, 2)) - { - if (response?.Content?.Headers == null || !response.IsSuccessStatusCode) - { - response?.Content?.Dispose(); - return null; - } - - var format = response.Content.Headers?.ContentType?.MediaType; - var bytes = await response.Content.ReadAsByteArrayAsync(); - response.Content.Dispose(); - if (format == null || !_allowedMediaTypes.Contains(format)) - { - if (HeaderMatch(bytes, _icoHeader)) - { - format = _icoMediaType; - } - else if (HeaderMatch(bytes, _pngHeader) || HeaderMatch(bytes, _webpHeader)) - { - format = _pngMediaType; - } - else if (HeaderMatch(bytes, _jpegHeader)) - { - format = _jpegMediaType; - } - else - { - return null; - } - } - - return new IconResult(uri, bytes, format); - } - } - - private async Task GetAndFollowAsync(Uri uri, int maxRedirectCount) - { - var response = await GetAsync(uri); - if (response == null) - { - return null; - } - return await FollowRedirectsAsync(response, maxRedirectCount); - } - - private async Task GetAsync(Uri uri) - { - if (uri == null) - { - return null; - } - - // Prevent non-http(s) and non-default ports - if ((uri.Scheme != "http" && uri.Scheme != "https") || !uri.IsDefaultPort) - { - return null; - } - - // Prevent local hosts (localhost, bobs-pc, etc) and IP addresses - if (!uri.Host.Contains(".") || IPAddress.TryParse(uri.Host, out _)) - { - return null; - } - - // Resolve host to make sure it is not an internal/private IP address - try - { - var hostEntry = Dns.GetHostEntry(uri.Host); - if (hostEntry?.AddressList.Any(ip => IsInternal(ip)) ?? true) - { - return null; - } - } - catch - { - return null; - } - - using (var message = new HttpRequestMessage()) - { - message.RequestUri = uri; - message.Method = HttpMethod.Get; - - // Let's add some headers to look like we're coming from a web browser request. Some websites - // will block our request without these. - message.Headers.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + - "(KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36 Edge/16.16299"); - message.Headers.Add("Accept-Language", "en-US,en;q=0.8"); - message.Headers.Add("Cache-Control", "no-cache"); - message.Headers.Add("Pragma", "no-cache"); - message.Headers.Add("Accept", "text/html,application/xhtml+xml,application/xml;" + - "q=0.9,image/webp,image/apng,*/*;q=0.8"); - - try - { - return await _httpClient.SendAsync(message); - } - catch - { - return null; - } - } - } - - private async Task FollowRedirectsAsync(HttpResponseMessage response, - int maxFollowCount, int followCount = 0) - { - if (response == null || response.IsSuccessStatusCode || followCount > maxFollowCount) - { - return response; - } - - if (!(response.StatusCode == HttpStatusCode.Redirect || - response.StatusCode == HttpStatusCode.MovedPermanently || - response.StatusCode == HttpStatusCode.RedirectKeepVerb || - response.StatusCode == HttpStatusCode.SeeOther) || - response.Headers.Location == null) - { - Cleanup(response); - return null; - } - - Uri location = null; - if (response.Headers.Location.IsAbsoluteUri) - { - if (response.Headers.Location.Scheme != "http" && response.Headers.Location.Scheme != "https") - { - if (Uri.TryCreate($"https://{response.Headers.Location.OriginalString}", - UriKind.Absolute, out var newUri)) - { - location = newUri; - } - } - else - { - location = response.Headers.Location; - } - } - else - { - var requestUri = response.RequestMessage.RequestUri; - location = ResolveUri($"{GetScheme(requestUri)}://{requestUri.Host}", - response.Headers.Location.OriginalString); - } - - Cleanup(response); - var newResponse = await GetAsync(location); - if (newResponse != null) - { - followCount++; - var redirectedResponse = await FollowRedirectsAsync(newResponse, maxFollowCount, followCount); - if (redirectedResponse != null) - { - if (redirectedResponse != newResponse) - { - Cleanup(newResponse); - } - return redirectedResponse; - } - } - return null; } - - private bool HeaderMatch(byte[] imageBytes, byte[] header) - { - return imageBytes.Length >= header.Length && header.SequenceEqual(imageBytes.Take(header.Length)); - } - - private Uri ResolveUri(string baseUrl, params string[] paths) - { - var url = baseUrl; - foreach (var path in paths) - { - if (Uri.TryCreate(new Uri(url), path, out var r)) - { - url = r.ToString(); - } - } - return new Uri(url); - } - - private void Cleanup(IDisposable obj) - { - obj?.Dispose(); - obj = null; - } - - private string GetScheme(Uri uri) - { - return uri != null && uri.Scheme == "http" ? "http" : "https"; - } - - public static bool IsInternal(IPAddress ip) - { - if (IPAddress.IsLoopback(ip)) - { - return true; - } - - var ipString = ip.ToString(); - if (ipString == "::1" || ipString == "::" || ipString.StartsWith("::ffff:")) - { - return true; - } - - // IPv6 - if (ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6) - { - return ipString.StartsWith("fc") || ipString.StartsWith("fd") || - ipString.StartsWith("fe") || ipString.StartsWith("ff"); - } - - // IPv4 - var bytes = ip.GetAddressBytes(); - return (bytes[0]) switch - { - 0 => true, - 10 => true, - 127 => true, - 169 => bytes[1] == 254, // Cloud environments, such as AWS - 172 => bytes[1] < 32 && bytes[1] >= 16, - 192 => bytes[1] == 168, - _ => false, - }; - } } diff --git a/src/Icons/Services/UriService.cs b/src/Icons/Services/UriService.cs new file mode 100644 index 0000000000..6be72315a6 --- /dev/null +++ b/src/Icons/Services/UriService.cs @@ -0,0 +1,109 @@ +#nullable enable + +using System.Net; +using System.Net.Sockets; +using Bit.Icons.Extensions; +using Bit.Icons.Models; + +namespace Bit.Icons.Services; + +public class UriService : IUriService +{ + public IconUri GetUri(string inputUri) + { + var uri = new Uri(inputUri); + return new IconUri(uri, DetermineIp(uri)); + } + + public bool TryGetUri(string stringUri, out IconUri? iconUri) + { + if (!Uri.TryCreate(stringUri, UriKind.Absolute, out var uri)) + { + iconUri = null; + return false; + } + + return TryGetUri(uri, out iconUri); + } + + public IconUri GetUri(Uri uri) + { + return new IconUri(uri, DetermineIp(uri)); + } + + public bool TryGetUri(Uri uri, out IconUri? iconUri) + { + try + { + iconUri = GetUri(uri); + return true; + } + catch (Exception) + { + iconUri = null; + return false; + } + } + + public IconUri GetRedirect(HttpResponseMessage response, IconUri originalUri) + { + if (response.Headers.Location == null) + { + throw new Exception("No redirect location found."); + } + + var redirectUri = DetermineRedirectUri(response.Headers.Location, originalUri); + return new IconUri(redirectUri, DetermineIp(redirectUri)); + } + + public bool TryGetRedirect(HttpResponseMessage response, IconUri originalUri, out IconUri? iconUri) + { + try + { + iconUri = GetRedirect(response, originalUri); + return true; + } + catch (Exception) + { + iconUri = null; + return false; + } + } + + private static Uri DetermineRedirectUri(Uri responseUri, IconUri originalIconUri) + { + if (responseUri.IsAbsoluteUri) + { + if (!responseUri.IsHypertext()) + { + return responseUri.ChangeScheme("https"); + } + return responseUri; + } + else + { + return new UriBuilder + { + Scheme = originalIconUri.Scheme, + Host = originalIconUri.Host, + Path = responseUri.ToString() + }.Uri; + } + } + + private static IPAddress DetermineIp(Uri uri) + { + if (IPAddress.TryParse(uri.Host, out var ip)) + { + return ip; + } + + var hostEntry = Dns.GetHostEntry(uri.Host); + ip = hostEntry.AddressList.FirstOrDefault(ip => ip.AddressFamily == AddressFamily.InterNetwork || ip.IsIPv4MappedToIPv6)?.MapToIPv4(); + if (ip == null) + { + throw new Exception($"Unable to determine IP for {uri.Host}"); + } + return ip; + } +} diff --git a/src/Icons/Startup.cs b/src/Icons/Startup.cs index f63407fa7a..2a7f83e136 100644 --- a/src/Icons/Startup.cs +++ b/src/Icons/Startup.cs @@ -1,7 +1,7 @@ using System.Globalization; using Bit.Core.Settings; using Bit.Core.Utilities; -using Bit.Icons.Services; +using Bit.Icons.Extensions; using Bit.SharedWeb.Utilities; using Microsoft.Net.Http.Headers; @@ -30,6 +30,12 @@ public class Startup ConfigurationBinder.Bind(Configuration.GetSection("IconsSettings"), iconsSettings); services.AddSingleton(s => iconsSettings); + // Http client + services.ConfigureHttpClients(); + + // Add HtmlParser + services.AddHtmlParsing(); + // Cache services.AddMemoryCache(options => { @@ -37,8 +43,7 @@ public class Startup }); // Services - services.AddSingleton(); - services.AddSingleton(); + services.AddServices(); // Mvc services.AddMvc(); diff --git a/src/Icons/Util/IPAddressExtension.cs b/src/Icons/Util/IPAddressExtension.cs new file mode 100644 index 0000000000..668548c5af --- /dev/null +++ b/src/Icons/Util/IPAddressExtension.cs @@ -0,0 +1,42 @@ +#nullable enable + +using System.Net; + +namespace Bit.Icons.Extensions; + +public static class IPAddressExtension +{ + public static bool IsInternal(this IPAddress ip) + { + if (IPAddress.IsLoopback(ip)) + { + return true; + } + + var ipString = ip.ToString(); + if (ipString == "::1" || ipString == "::" || ipString.StartsWith("::ffff:")) + { + return true; + } + + // IPv6 + if (ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6) + { + return ipString.StartsWith("fc") || ipString.StartsWith("fd") || + ipString.StartsWith("fe") || ipString.StartsWith("ff"); + } + + // IPv4 + var bytes = ip.GetAddressBytes(); + return (bytes[0]) switch + { + 0 => true, + 10 => true, + 127 => true, + 169 => bytes[1] == 254, // Cloud environments, such as AWS + 172 => bytes[1] < 32 && bytes[1] >= 16, + 192 => bytes[1] == 168, + _ => false, + }; + } +} diff --git a/src/Icons/Util/ServiceCollectionExtension.cs b/src/Icons/Util/ServiceCollectionExtension.cs new file mode 100644 index 0000000000..5492cda0cf --- /dev/null +++ b/src/Icons/Util/ServiceCollectionExtension.cs @@ -0,0 +1,44 @@ +# nullable enable + +using System.Net; +using AngleSharp.Html.Parser; +using Bit.Icons.Services; + +namespace Bit.Icons.Extensions; + +public static class ServiceCollectionExtension +{ + public static void ConfigureHttpClients(this IServiceCollection services) + { + services.AddHttpClient("Icons", client => + { + client.Timeout = TimeSpan.FromSeconds(20); + client.MaxResponseContentBufferSize = 5000000; // 5 MB + // Let's add some headers to look like we're coming from a web browser request. Some websites + // will block our request without these. + client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + + "(KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36"); + client.DefaultRequestHeaders.Add("Accept-Language", "en-US,en;q=0.8"); + client.DefaultRequestHeaders.Add("Cache-Control", "no-cache"); + client.DefaultRequestHeaders.Add("Pragma", "no-cache"); + client.DefaultRequestHeaders.Add("Accept", "text/html,application/xhtml+xml,application/xml;" + + "q=0.9,image/webp,image/apng,*/*;q=0.8"); + }).ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler + { + AllowAutoRedirect = false, + AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate, + }); + } + + public static void AddHtmlParsing(this IServiceCollection services) + { + services.AddSingleton(); + } + + public static void AddServices(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + } +} diff --git a/src/Icons/Util/UriBuilderExtension.cs b/src/Icons/Util/UriBuilderExtension.cs new file mode 100644 index 0000000000..7c4ac538a4 --- /dev/null +++ b/src/Icons/Util/UriBuilderExtension.cs @@ -0,0 +1,20 @@ +#nullable enable + +namespace Bit.Icons.Extensions; + +public static class UriBuilderExtension +{ + public static bool TryBuild(this UriBuilder builder, out Uri? uri) + { + try + { + uri = builder.Uri; + return true; + } + catch (UriFormatException) + { + uri = null; + return false; + } + } +} diff --git a/src/Icons/Util/UriExtension.cs b/src/Icons/Util/UriExtension.cs new file mode 100644 index 0000000000..432db96a1d --- /dev/null +++ b/src/Icons/Util/UriExtension.cs @@ -0,0 +1,41 @@ + +#nullable enable + +namespace Bit.Icons.Extensions; + +public static class UriExtension +{ + public static bool IsHypertext(this Uri uri) + { + return uri.Scheme == "http" || uri.Scheme == "https"; + } + + public static Uri ChangeScheme(this Uri uri, string scheme) + { + return new UriBuilder(scheme, uri.Host) { Path = uri.PathAndQuery }.Uri; + } + + public static Uri ChangeHost(this Uri uri, string host) + { + return new UriBuilder(uri) { Host = host }.Uri; + } + + public static Uri ConcatPath(this Uri uri, params string[] paths) + => uri.ConcatPath(paths.AsEnumerable()); + public static Uri ConcatPath(this Uri uri, IEnumerable paths) + { + if (!paths.Any()) + { + return uri; + } + + if (Uri.TryCreate(uri, paths.First(), out var newUri)) + { + return newUri.ConcatPath(paths.Skip(1)); + } + else + { + return uri; + } + } +} diff --git a/src/Icons/packages.lock.json b/src/Icons/packages.lock.json index 14728b06a4..eaf9913dc4 100644 --- a/src/Icons/packages.lock.json +++ b/src/Icons/packages.lock.json @@ -4,12 +4,11 @@ "net6.0": { "AngleSharp": { "type": "Direct", - "requested": "[0.16.1, )", - "resolved": "0.16.1", - "contentHash": "1k7Vbfmr5IUsGaR0QJwTe8XF9zacFUIoWxMgI4X/ipiyKxCWZJZoaG96fNEugL90iubvboRvE1IxuBPibET/Rg==", + "requested": "[1.0.4, )", + "resolved": "1.0.4", + "contentHash": "G8R4C2MEDFQvjUbYz1QIcGfibjsTJnzP0aWy8iQgWWk7eKacYydCNGD3JMhVL0Q5pZQ9RYlqpKNESEU5NpqsRw==", "dependencies": { - "System.Buffers": "4.5.1", - "System.Text.Encoding.CodePages": "5.0.0" + "System.Text.Encoding.CodePages": "6.0.0" } }, "AspNetCoreRateLimit": { @@ -2379,10 +2378,10 @@ }, "System.Text.Encoding.CodePages": { "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "NyscU59xX6Uo91qvhOs2Ccho3AR2TnZPomo1Z0K6YpyztBPM/A5VbkzOO19sy3A3i1TtEnTxA7bCe3Us+r5MWg==", + "resolved": "6.0.0", + "contentHash": "ZFCILZuOvtKPauZ/j/swhvw68ZRi9ATCfvGbk1QfydmcXBkIWecWKn/250UH7rahZ5OoDBaiAudJtPvLwzw85A==", "dependencies": { - "Microsoft.NETCore.Platforms": "5.0.0" + "System.Runtime.CompilerServices.Unsafe": "6.0.0" } }, "System.Text.Encoding.Extensions": { diff --git a/test/Api.IntegrationTest/SecretsManager/Controllers/ProjectsControllerTests.cs b/test/Api.IntegrationTest/SecretsManager/Controllers/ProjectsControllerTests.cs index 05bdda7137..0ef31085f2 100644 --- a/test/Api.IntegrationTest/SecretsManager/Controllers/ProjectsControllerTests.cs +++ b/test/Api.IntegrationTest/SecretsManager/Controllers/ProjectsControllerTests.cs @@ -10,6 +10,7 @@ using Bit.Core.Enums; using Bit.Core.SecretsManager.Entities; using Bit.Core.SecretsManager.Repositories; using Bit.Test.Common.Helpers; +using Pipelines.Sockets.Unofficial.Arenas; using Xunit; namespace Bit.Api.IntegrationTest.SecretsManager.Controllers; @@ -295,6 +296,25 @@ public class ProjectsControllerTests : IClassFixture, IAs Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } + [Fact] + public async Task Get_NonExistingProject_NotFound() + { + var (org, _) = await _organizationHelper.Initialize(true, true); + var (email, _) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true); + await LoginAsync(email); + + var createdProject = await _projectRepository.CreateAsync(new Project + { + OrganizationId = org.Id, + Name = _mockEncryptedString, + }); + + var deleteResponse = await _client.PostAsync("/projects/delete", JsonContent.Create(createdProject.Id)); + + var response = await _client.GetAsync($"/projects/{createdProject.Id}"); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + [Theory] [InlineData(PermissionType.RunAsAdmin)] [InlineData(PermissionType.RunAsUserWithPermission)] diff --git a/test/Api.Test/Controllers/CollectionsControllerTests.cs b/test/Api.Test/Controllers/CollectionsControllerTests.cs index 8a3f944ff5..c035a8bc51 100644 --- a/test/Api.Test/Controllers/CollectionsControllerTests.cs +++ b/test/Api.Test/Controllers/CollectionsControllerTests.cs @@ -171,7 +171,7 @@ public class CollectionsControllerTests .Returns(user.Id); sutProvider.GetDependency() - .GetOrganizationCollections(orgId) + .GetOrganizationCollectionsAsync(orgId) .Returns(collections); // Act @@ -237,7 +237,7 @@ public class CollectionsControllerTests .Returns(user.Id); sutProvider.GetDependency() - .GetOrganizationCollections(orgId) + .GetOrganizationCollectionsAsync(orgId) .Returns(collections); // Act diff --git a/test/Api.Test/packages.lock.json b/test/Api.Test/packages.lock.json index dec56bd455..7f490ffb18 100644 --- a/test/Api.Test/packages.lock.json +++ b/test/Api.Test/packages.lock.json @@ -1181,15 +1181,6 @@ "System.Security.Cryptography.Pkcs": "6.0.0" } }, - "Moq": { - "type": "Transitive", - "resolved": "4.17.2", - "contentHash": "HytUPJ3/uks2UgJ9hIcyXm3YxpFAR4OJzbQwTHltbKGun3lFLhEHs97hiiPj1dY8jV/kasXeihTzDxct6Zf3iQ==", - "dependencies": { - "Castle.Core": "4.4.1", - "System.Threading.Tasks.Extensions": "4.5.4" - } - }, "MySqlConnector": { "type": "Transitive", "resolved": "2.2.5", @@ -3015,129 +3006,128 @@ "api": { "type": "Project", "dependencies": { - "AspNetCore.HealthChecks.AzureServiceBus": "6.1.0", - "AspNetCore.HealthChecks.AzureStorage": "6.1.2", - "AspNetCore.HealthChecks.Network": "6.0.4", - "AspNetCore.HealthChecks.Redis": "6.0.4", - "AspNetCore.HealthChecks.SendGrid": "6.0.2", - "AspNetCore.HealthChecks.SqlServer": "6.0.2", - "AspNetCore.HealthChecks.Uris": "6.0.3", - "Azure.Messaging.EventGrid": "4.10.0", - "Commercial.Core": "2023.7.2", - "Commercial.Infrastructure.EntityFramework": "2023.7.2", - "Core": "2023.7.2", - "SharedWeb": "2023.7.2", - "Swashbuckle.AspNetCore": "6.5.0" + "AspNetCore.HealthChecks.AzureServiceBus": "[6.1.0, )", + "AspNetCore.HealthChecks.AzureStorage": "[6.1.2, )", + "AspNetCore.HealthChecks.Network": "[6.0.4, )", + "AspNetCore.HealthChecks.Redis": "[6.0.4, )", + "AspNetCore.HealthChecks.SendGrid": "[6.0.2, )", + "AspNetCore.HealthChecks.SqlServer": "[6.0.2, )", + "AspNetCore.HealthChecks.Uris": "[6.0.3, )", + "Azure.Messaging.EventGrid": "[4.10.0, )", + "Commercial.Core": "[2023.7.2, )", + "Commercial.Infrastructure.EntityFramework": "[2023.7.2, )", + "Core": "[2023.7.2, )", + "SharedWeb": "[2023.7.2, )", + "Swashbuckle.AspNetCore": "[6.5.0, )" } }, "commercial.core": { "type": "Project", "dependencies": { - "Core": "2023.7.2" + "Core": "[2023.7.2, )" } }, "commercial.infrastructure.entityframework": { "type": "Project", "dependencies": { - "AutoMapper.Extensions.Microsoft.DependencyInjection": "12.0.1", - "Core": "2023.7.2", - "Infrastructure.EntityFramework": "2023.7.2" + "AutoMapper.Extensions.Microsoft.DependencyInjection": "[12.0.1, )", + "Core": "[2023.7.2, )", + "Infrastructure.EntityFramework": "[2023.7.2, )" } }, "common": { "type": "Project", "dependencies": { - "AutoFixture.AutoNSubstitute": "4.17.0", - "AutoFixture.Xunit2": "4.17.0", - "Core": "2023.7.2", - "Kralizek.AutoFixture.Extensions.MockHttp": "1.2.0", - "Microsoft.NET.Test.Sdk": "17.1.0", - "NSubstitute": "4.3.0", - "xunit": "2.4.1" + "AutoFixture.AutoNSubstitute": "[4.17.0, )", + "AutoFixture.Xunit2": "[4.17.0, )", + "Core": "[2023.7.2, )", + "Kralizek.AutoFixture.Extensions.MockHttp": "[1.2.0, )", + "Microsoft.NET.Test.Sdk": "[17.1.0, )", + "NSubstitute": "[4.3.0, )", + "xunit": "[2.4.1, )" } }, "core": { "type": "Project", "dependencies": { - "AWSSDK.SQS": "3.7.2.47", - "AWSSDK.SimpleEmail": "3.7.0.150", - "AspNetCoreRateLimit": "4.0.2", - "AspNetCoreRateLimit.Redis": "1.0.1", - "Azure.Extensions.AspNetCore.DataProtection.Blobs": "1.3.2", - "Azure.Messaging.ServiceBus": "7.15.0", - "Azure.Storage.Blobs": "12.14.1", - "Azure.Storage.Queues": "12.12.0", - "BitPay.Light": "1.0.1907", - "Braintree": "5.12.0", - "DnsClient": "1.7.0", - "Fido2.AspNet": "3.0.1", - "Handlebars.Net": "2.1.2", - "IdentityServer4": "4.1.2", - "IdentityServer4.AccessTokenValidation": "3.0.1", - "LaunchDarkly.ServerSdk": "7.0.0", - "MailKit": "3.2.0", - "Microsoft.AspNetCore.Authentication.JwtBearer": "6.0.4", - "Microsoft.Azure.Cosmos.Table": "1.0.8", - "Microsoft.Azure.NotificationHubs": "4.1.0", - "Microsoft.Data.SqlClient": "5.0.1", - "Microsoft.Extensions.Caching.StackExchangeRedis": "6.0.6", - "Microsoft.Extensions.Configuration.EnvironmentVariables": "6.0.1", - "Microsoft.Extensions.Configuration.UserSecrets": "6.0.1", - "Microsoft.Extensions.Identity.Stores": "6.0.4", - "Newtonsoft.Json": "13.0.1", - "Otp.NET": "1.2.2", - "Quartz": "3.4.0", - "SendGrid": "9.27.0", - "Sentry.Serilog": "3.16.0", - "Serilog.AspNetCore": "5.0.0", - "Serilog.Extensions.Logging": "3.1.0", - "Serilog.Extensions.Logging.File": "2.0.0", - "Serilog.Sinks.AzureCosmosDB": "2.0.0", - "Serilog.Sinks.SyslogMessages": "2.0.6", - "Stripe.net": "40.0.0", - "YubicoDotNetClient": "1.2.0" + "AWSSDK.SQS": "[3.7.2.47, )", + "AWSSDK.SimpleEmail": "[3.7.0.150, )", + "AspNetCoreRateLimit": "[4.0.2, )", + "AspNetCoreRateLimit.Redis": "[1.0.1, )", + "Azure.Extensions.AspNetCore.DataProtection.Blobs": "[1.3.2, )", + "Azure.Messaging.ServiceBus": "[7.15.0, )", + "Azure.Storage.Blobs": "[12.14.1, )", + "Azure.Storage.Queues": "[12.12.0, )", + "BitPay.Light": "[1.0.1907, )", + "Braintree": "[5.12.0, )", + "DnsClient": "[1.7.0, )", + "Fido2.AspNet": "[3.0.1, )", + "Handlebars.Net": "[2.1.2, )", + "IdentityServer4": "[4.1.2, )", + "IdentityServer4.AccessTokenValidation": "[3.0.1, )", + "LaunchDarkly.ServerSdk": "[7.0.0, )", + "MailKit": "[3.2.0, )", + "Microsoft.AspNetCore.Authentication.JwtBearer": "[6.0.4, )", + "Microsoft.Azure.Cosmos.Table": "[1.0.8, )", + "Microsoft.Azure.NotificationHubs": "[4.1.0, )", + "Microsoft.Data.SqlClient": "[5.0.1, )", + "Microsoft.Extensions.Caching.StackExchangeRedis": "[6.0.6, )", + "Microsoft.Extensions.Configuration.EnvironmentVariables": "[6.0.1, )", + "Microsoft.Extensions.Configuration.UserSecrets": "[6.0.1, )", + "Microsoft.Extensions.Identity.Stores": "[6.0.4, )", + "Newtonsoft.Json": "[13.0.1, )", + "Otp.NET": "[1.2.2, )", + "Quartz": "[3.4.0, )", + "SendGrid": "[9.27.0, )", + "Sentry.Serilog": "[3.16.0, )", + "Serilog.AspNetCore": "[5.0.0, )", + "Serilog.Extensions.Logging": "[3.1.0, )", + "Serilog.Extensions.Logging.File": "[2.0.0, )", + "Serilog.Sinks.AzureCosmosDB": "[2.0.0, )", + "Serilog.Sinks.SyslogMessages": "[2.0.6, )", + "Stripe.net": "[40.0.0, )", + "YubicoDotNetClient": "[1.2.0, )" } }, "core.test": { "type": "Project", "dependencies": { - "AutoFixture.AutoNSubstitute": "4.17.0", - "AutoFixture.Xunit2": "4.17.0", - "Common": "2023.7.2", - "Core": "2023.7.2", - "Kralizek.AutoFixture.Extensions.MockHttp": "1.2.0", - "Microsoft.NET.Test.Sdk": "17.1.0", - "Moq": "4.17.2", - "NSubstitute": "4.3.0", - "xunit": "2.4.1" + "AutoFixture.AutoNSubstitute": "[4.17.0, )", + "AutoFixture.Xunit2": "[4.17.0, )", + "Common": "[2023.7.2, )", + "Core": "[2023.7.2, )", + "Kralizek.AutoFixture.Extensions.MockHttp": "[1.2.0, )", + "Microsoft.NET.Test.Sdk": "[17.1.0, )", + "NSubstitute": "[4.3.0, )", + "xunit": "[2.4.1, )" } }, "infrastructure.dapper": { "type": "Project", "dependencies": { - "Core": "2023.7.2", - "Dapper": "2.0.123" + "Core": "[2023.7.2, )", + "Dapper": "[2.0.123, )" } }, "infrastructure.entityframework": { "type": "Project", "dependencies": { - "AutoMapper.Extensions.Microsoft.DependencyInjection": "12.0.1", - "Core": "2023.7.2", - "Microsoft.EntityFrameworkCore.Relational": "7.0.5", - "Microsoft.EntityFrameworkCore.SqlServer": "7.0.5", - "Microsoft.EntityFrameworkCore.Sqlite": "7.0.5", - "Npgsql.EntityFrameworkCore.PostgreSQL": "7.0.4", - "Pomelo.EntityFrameworkCore.MySql": "7.0.0", - "linq2db.EntityFrameworkCore": "7.5.0" + "AutoMapper.Extensions.Microsoft.DependencyInjection": "[12.0.1, )", + "Core": "[2023.7.2, )", + "Microsoft.EntityFrameworkCore.Relational": "[7.0.5, )", + "Microsoft.EntityFrameworkCore.SqlServer": "[7.0.5, )", + "Microsoft.EntityFrameworkCore.Sqlite": "[7.0.5, )", + "Npgsql.EntityFrameworkCore.PostgreSQL": "[7.0.4, )", + "Pomelo.EntityFrameworkCore.MySql": "[7.0.0, )", + "linq2db.EntityFrameworkCore": "[7.5.0, )" } }, "sharedweb": { "type": "Project", "dependencies": { - "Core": "2023.7.2", - "Infrastructure.Dapper": "2023.7.2", - "Infrastructure.EntityFramework": "2023.7.2" + "Core": "[2023.7.2, )", + "Infrastructure.Dapper": "[2023.7.2, )", + "Infrastructure.EntityFramework": "[2023.7.2, )" } } } diff --git a/test/Common/Helpers/HtmlBuilder.cs b/test/Common/Helpers/HtmlBuilder.cs new file mode 100644 index 0000000000..92edd9505c --- /dev/null +++ b/test/Common/Helpers/HtmlBuilder.cs @@ -0,0 +1,59 @@ +using System.Text; + +namespace Bit.Test.Common.Helpers; + +public class HtmlBuilder +{ + private string _topLevelNode; + private readonly StringBuilder _builder = new(); + + public HtmlBuilder(string topLevelNode = "html") + { + _topLevelNode = CoerceTopLevelNode(topLevelNode); + } + + public HtmlBuilder Append(string node) + { + _builder.Append(node); + return this; + } + + public HtmlBuilder Append(HtmlBuilder builder) + { + _builder.Append(builder.ToString()); + return this; + } + + public HtmlBuilder WithAttribute(string name, string value) + { + _topLevelNode = $"{_topLevelNode} {name}=\"{value}\""; + return this; + } + + public override string ToString() + { + _builder.Insert(0, $"<{_topLevelNode}>"); + _builder.Append($""); + return _builder.ToString(); + } + + private static string CoerceTopLevelNode(string topLevelNode) + { + var result = topLevelNode; + if (topLevelNode.StartsWith("<")) + { + result = topLevelNode[1..]; + } + if (topLevelNode.EndsWith(">")) + { + result = result[..^1]; + } + + if (topLevelNode.IndexOf(">") != -1) + { + throw new ArgumentException("Top level nodes cannot contain '>' characters."); + } + + return result; + } +} diff --git a/test/Common/MockedHttpClient/HttpRequestMatcher.cs b/test/Common/MockedHttpClient/HttpRequestMatcher.cs new file mode 100644 index 0000000000..7e4d0d2dab --- /dev/null +++ b/test/Common/MockedHttpClient/HttpRequestMatcher.cs @@ -0,0 +1,104 @@ +#nullable enable + +using System.Net; + +namespace Bit.Test.Common.MockedHttpClient; + +public class HttpRequestMatcher : IHttpRequestMatcher +{ + private readonly Func _matcher; + private HttpRequestMatcher? _childMatcher; + private MockedHttpResponse _mockedResponse = new(HttpStatusCode.OK); + private bool _responseSpecified = false; + + public int NumberOfMatches { get; private set; } + + /// + /// Returns whether or not the provided request can be handled by this matcher chain. + /// + /// + /// + public bool Matches(HttpRequestMessage request) => _matcher(request) && (_childMatcher == null || _childMatcher.Matches(request)); + + public HttpRequestMatcher(HttpMethod method) + { + _matcher = request => request.Method == method; + } + + public HttpRequestMatcher(string uri) + { + _matcher = request => request.RequestUri == new Uri(uri); + } + + public HttpRequestMatcher(Uri uri) + { + _matcher = request => request.RequestUri == uri; + } + + public HttpRequestMatcher(HttpMethod method, string uri) + { + _matcher = request => request.Method == method && request.RequestUri == new Uri(uri); + } + + public HttpRequestMatcher(Func matcher) + { + _matcher = matcher; + } + + public HttpRequestMatcher WithHeader(string name, string value) + { + return AddChild(request => request.Headers.TryGetValues(name, out var values) && values.Contains(value)); + } + + public HttpRequestMatcher WithQueryParameters(Dictionary requiredQueryParameters) => + WithQueryParameters(requiredQueryParameters.Select(x => $"{x.Key}={x.Value}").ToArray()); + public HttpRequestMatcher WithQueryParameters(string name, string value) => + WithQueryParameters($"{name}={value}"); + public HttpRequestMatcher WithQueryParameters(params string[] queryKeyValues) + { + bool matcher(HttpRequestMessage request) + { + var query = request.RequestUri?.Query; + if (query == null) + { + return false; + } + + return queryKeyValues.All(queryKeyValue => query.Contains(queryKeyValue)); + } + return AddChild(matcher); + } + + /// + /// Configure how this matcher should respond to matching HttpRequestMessages. + /// Note, after specifying a response, you can no longer further specify match criteria. + /// + /// + /// + public MockedHttpResponse RespondWith(HttpStatusCode statusCode) + { + _responseSpecified = true; + _mockedResponse = new MockedHttpResponse(statusCode); + return _mockedResponse; + } + + /// + /// Called to produce an HttpResponseMessage for the given request. This is probably something you want to leave alone + /// + /// + public async Task RespondToAsync(HttpRequestMessage request) + { + NumberOfMatches++; + return await (_childMatcher == null ? _mockedResponse.RespondToAsync(request) : _childMatcher.RespondToAsync(request)); + } + + private HttpRequestMatcher AddChild(Func matcher) + { + if (_responseSpecified) + { + throw new Exception("Cannot continue to configure a matcher after a response has been specified"); + } + _childMatcher = new HttpRequestMatcher(matcher); + return _childMatcher; + } +} diff --git a/test/Common/MockedHttpClient/HttpResponseBuilder.cs b/test/Common/MockedHttpClient/HttpResponseBuilder.cs new file mode 100644 index 0000000000..067defb6d2 --- /dev/null +++ b/test/Common/MockedHttpClient/HttpResponseBuilder.cs @@ -0,0 +1,84 @@ +using System.Net; + +namespace Bit.Test.Common.MockedHttpClient; + +public class HttpResponseBuilder : IDisposable +{ + private bool _disposedValue; + + public HttpStatusCode StatusCode { get; set; } + public IEnumerable> Headers { get; set; } = new List>(); + public IEnumerable HeadersToRemove { get; set; } = new List(); + public HttpContent Content { get; set; } + + public async Task ToHttpResponseAsync() + { + var copiedContentStream = new MemoryStream(); + await Content.CopyToAsync(copiedContentStream); // This is important, otherwise the content stream will be disposed when the response is disposed. + copiedContentStream.Seek(0, SeekOrigin.Begin); + var message = new HttpResponseMessage(StatusCode) + { + Content = new StreamContent(copiedContentStream), + }; + + foreach (var header in Headers) + { + message.Headers.TryAddWithoutValidation(header.Key, header.Value); + } + + return message; + } + + public HttpResponseBuilder WithStatusCode(HttpStatusCode statusCode) + { + return new() + { + StatusCode = statusCode, + Headers = Headers, + HeadersToRemove = HeadersToRemove, + Content = Content, + }; + } + + public HttpResponseBuilder WithHeader(string name, string value) + { + return new() + { + StatusCode = StatusCode, + Headers = Headers.Append(new KeyValuePair(name, value)), + HeadersToRemove = HeadersToRemove, + Content = Content, + }; + } + + public HttpResponseBuilder WithContent(HttpContent content) + { + return new() + { + StatusCode = StatusCode, + Headers = Headers, + HeadersToRemove = HeadersToRemove, + Content = content, + }; + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + Content?.Dispose(); + } + + _disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} diff --git a/test/Common/MockedHttpClient/IHttpRequestMatcher.cs b/test/Common/MockedHttpClient/IHttpRequestMatcher.cs new file mode 100644 index 0000000000..e8de78b075 --- /dev/null +++ b/test/Common/MockedHttpClient/IHttpRequestMatcher.cs @@ -0,0 +1,10 @@ +#nullable enable + +namespace Bit.Test.Common.MockedHttpClient; + +public interface IHttpRequestMatcher +{ + int NumberOfMatches { get; } + bool Matches(HttpRequestMessage request); + Task RespondToAsync(HttpRequestMessage request); +} diff --git a/test/Common/MockedHttpClient/IMockedHttpResponse.cs b/test/Common/MockedHttpClient/IMockedHttpResponse.cs new file mode 100644 index 0000000000..a836cb8af4 --- /dev/null +++ b/test/Common/MockedHttpClient/IMockedHttpResponse.cs @@ -0,0 +1,7 @@ +namespace Bit.Test.Common.MockedHttpClient; + +public interface IMockedHttpResponse +{ + int NumberOfResponses { get; } + Task RespondToAsync(HttpRequestMessage request); +} diff --git a/test/Common/MockedHttpClient/MockedHttpMessageHandler.cs b/test/Common/MockedHttpClient/MockedHttpMessageHandler.cs new file mode 100644 index 0000000000..1b1bd52a03 --- /dev/null +++ b/test/Common/MockedHttpClient/MockedHttpMessageHandler.cs @@ -0,0 +1,113 @@ +#nullable enable + +using System.Net; + +namespace Bit.Test.Common.MockedHttpClient; + +public class MockedHttpMessageHandler : HttpMessageHandler +{ + private readonly List _matchers = new(); + + /// + /// The fallback handler to use when the request does not match any of the provided matchers. + /// + /// A Matcher that responds with 404 Not Found + public MockedHttpResponse Fallback { get; set; } = new(HttpStatusCode.NotFound); + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var matcher = _matchers.FirstOrDefault(x => x.Matches(request)); + if (matcher == null) + { + return await Fallback.RespondToAsync(request); + } + + return await matcher.RespondToAsync(request); + } + + /// + /// Instantiates a new HttpRequestMessage matcher that will handle requests in fitting with the returned matcher. Configuration can be chained. + /// + /// + /// + /// + public T When(T requestMatcher) where T : IHttpRequestMatcher + { + _matchers.Add(requestMatcher); + return requestMatcher; + } + + /// + /// Instantiates a new HttpRequestMessage matcher that will handle requests in fitting with the returned matcher. Configuration can be chained. + /// + /// + /// + /// + public HttpRequestMatcher When(string uri) + { + var matcher = new HttpRequestMatcher(uri); + _matchers.Add(matcher); + return matcher; + } + + /// + /// Instantiates a new HttpRequestMessage matcher that will handle requests in fitting with the returned matcher. Configuration can be chained. + /// + /// + /// + /// + public HttpRequestMatcher When(Uri uri) + { + var matcher = new HttpRequestMatcher(uri); + _matchers.Add(matcher); + return matcher; + } + + /// + /// Instantiates a new HttpRequestMessage matcher that will handle requests in fitting with the returned matcher. Configuration can be chained. + /// + /// + /// + /// + public HttpRequestMatcher When(HttpMethod method) + { + var matcher = new HttpRequestMatcher(method); + _matchers.Add(matcher); + return matcher; + } + + /// + /// Instantiates a new HttpRequestMessage matcher that will handle requests in fitting with the returned matcher. Configuration can be chained. + /// + /// + /// + /// + public HttpRequestMatcher When(HttpMethod method, string uri) + { + var matcher = new HttpRequestMatcher(method, uri); + _matchers.Add(matcher); + return matcher; + } + + /// + /// Instantiates a new HttpRequestMessage matcher that will handle requests in fitting with the returned matcher. Configuration can be chained. + /// + /// + /// + /// + public HttpRequestMatcher When(Func matcher) + { + var requestMatcher = new HttpRequestMatcher(matcher); + _matchers.Add(requestMatcher); + return requestMatcher; + } + + /// + /// Converts the MockedHttpMessageHandler to a HttpClient that can be used in your tests after setup. + /// + /// + public HttpClient ToHttpClient() + { + return new HttpClient(this); + } +} diff --git a/test/Common/MockedHttpClient/MockedHttpResponse.cs b/test/Common/MockedHttpClient/MockedHttpResponse.cs new file mode 100644 index 0000000000..499807c615 --- /dev/null +++ b/test/Common/MockedHttpClient/MockedHttpResponse.cs @@ -0,0 +1,68 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Text; + +namespace Bit.Test.Common.MockedHttpClient; + +public class MockedHttpResponse : IMockedHttpResponse +{ + private MockedHttpResponse _childResponse; + private readonly Func _responder; + + public int NumberOfResponses { get; private set; } + + public MockedHttpResponse(HttpStatusCode statusCode) + { + _responder = (_, builder) => builder.WithStatusCode(statusCode); + } + + private MockedHttpResponse(Func responder) + { + _responder = responder; + } + + public MockedHttpResponse WithStatusCode(HttpStatusCode statusCode) + { + return AddChild((_, builder) => builder.WithStatusCode(statusCode)); + } + + public MockedHttpResponse WithHeader(string name, string value) + { + return AddChild((_, builder) => builder.WithHeader(name, value)); + } + public MockedHttpResponse WithHeaders(params KeyValuePair[] headers) + { + return AddChild((_, builder) => headers.Aggregate(builder, (b, header) => b.WithHeader(header.Key, header.Value))); + } + + public MockedHttpResponse WithContent(string mediaType, string content) + { + return WithContent(new StringContent(content, Encoding.UTF8, mediaType)); + } + public MockedHttpResponse WithContent(string mediaType, byte[] content) + { + return WithContent(new ByteArrayContent(content) { Headers = { ContentType = new MediaTypeHeaderValue(mediaType) } }); + } + public MockedHttpResponse WithContent(HttpContent content) + { + return AddChild((_, builder) => builder.WithContent(content)); + } + + public async Task RespondToAsync(HttpRequestMessage request) + { + return await RespondToAsync(request, new HttpResponseBuilder()); + } + + private async Task RespondToAsync(HttpRequestMessage request, HttpResponseBuilder currentBuilder) + { + NumberOfResponses++; + var nextBuilder = _responder(request, currentBuilder); + return await (_childResponse == null ? nextBuilder.ToHttpResponseAsync() : _childResponse.RespondToAsync(request, nextBuilder)); + } + + private MockedHttpResponse AddChild(Func responder) + { + _childResponse = new MockedHttpResponse(responder); + return _childResponse; + } +} diff --git a/test/Core.Test/AutoFixture/GlobalSettingsFixtures.cs b/test/Core.Test/AutoFixture/GlobalSettingsFixtures.cs index 39f57389ac..020b097077 100644 --- a/test/Core.Test/AutoFixture/GlobalSettingsFixtures.cs +++ b/test/Core.Test/AutoFixture/GlobalSettingsFixtures.cs @@ -6,7 +6,7 @@ using AutoFixture.Xunit2; using Bit.Core; using Bit.Core.Test.Helpers.Factories; using Microsoft.AspNetCore.DataProtection; -using Moq; +using NSubstitute; namespace Bit.Test.Common.AutoFixture; @@ -33,17 +33,17 @@ public class GlobalSettingsBuilder : ISpecimenBuilder if (pi.ParameterType == typeof(IDataProtectionProvider)) { - var dataProtector = new Mock(); - dataProtector - .Setup(d => d.Unprotect(It.IsAny())) - .Returns(data => Encoding.UTF8.GetBytes(Constants.DatabaseFieldProtectedPrefix + Encoding.UTF8.GetString(data))); + var dataProtector = Substitute.For(); + dataProtector.Unprotect(Arg.Any()) + .Returns(data => + Encoding.UTF8.GetBytes(Constants.DatabaseFieldProtectedPrefix + + Encoding.UTF8.GetString((byte[])data[0]))); - var dataProtectionProvider = new Mock(); - dataProtectionProvider - .Setup(x => x.CreateProtector(Constants.DatabaseFieldProtectorPurpose)) - .Returns(dataProtector.Object); + var dataProtectionProvider = Substitute.For(); + dataProtectionProvider.CreateProtector(Constants.DatabaseFieldProtectorPurpose) + .Returns(dataProtector); - return dataProtectionProvider.Object; + return dataProtectionProvider; } return new NoSpecimen(); diff --git a/test/Core.Test/Core.Test.csproj b/test/Core.Test/Core.Test.csproj index 875230cfea..d38040363e 100644 --- a/test/Core.Test/Core.Test.csproj +++ b/test/Core.Test/Core.Test.csproj @@ -9,7 +9,6 @@ all - diff --git a/test/Core.Test/Services/CollectionServiceTests.cs b/test/Core.Test/Services/CollectionServiceTests.cs index 4577e591db..d5b5f15ccd 100644 --- a/test/Core.Test/Services/CollectionServiceTests.cs +++ b/test/Core.Test/Services/CollectionServiceTests.cs @@ -1,4 +1,5 @@ -using Bit.Core.Entities; +using Bit.Core.Context; +using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Data; @@ -185,4 +186,56 @@ public class CollectionServiceTest .LogOrganizationUserEventAsync(default, default); } + [Theory, BitAutoData] + public async Task GetOrganizationCollectionsAsync_WithViewAssignedCollectionsTrue_ReturnsAssignedCollections( + CollectionDetails collectionDetails, Guid organizationId, Guid userId, SutProvider sutProvider) + { + collectionDetails.OrganizationId = organizationId; + + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency() + .GetManyByUserIdAsync(userId) + .Returns(new List { collectionDetails }); + sutProvider.GetDependency().ViewAssignedCollections(organizationId).Returns(true); + + var result = await sutProvider.Sut.GetOrganizationCollectionsAsync(organizationId); + + Assert.Single(result); + Assert.Equal(collectionDetails, result.First()); + + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetManyByOrganizationIdAsync(default); + await sutProvider.GetDependency().Received(1).GetManyByUserIdAsync(userId); + } + + [Theory, BitAutoData] + public async Task GetOrganizationCollectionsAsync_WithViewAllCollectionsTrue_ReturnsAllOrganizationCollections( + Collection collection, Guid organizationId, Guid userId, SutProvider sutProvider) + { + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency() + .GetManyByOrganizationIdAsync(organizationId) + .Returns(new List { collection }); + sutProvider.GetDependency().ViewAssignedCollections(organizationId).Returns(true); + sutProvider.GetDependency().ViewAllCollections(organizationId).Returns(true); + + var result = await sutProvider.Sut.GetOrganizationCollectionsAsync(organizationId); + + Assert.Single(result); + Assert.Equal(collection, result.First()); + + await sutProvider.GetDependency().Received(1).GetManyByOrganizationIdAsync(organizationId); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetManyByUserIdAsync(default); + } + + [Theory, BitAutoData] + public async Task GetOrganizationCollectionsAsync_WithViewAssignedCollectionsFalse_ThrowsBadRequestException( + Guid organizationId, SutProvider sutProvider) + { + sutProvider.GetDependency().ViewAssignedCollections(organizationId).Returns(false); + + await Assert.ThrowsAsync(() => sutProvider.Sut.GetOrganizationCollectionsAsync(organizationId)); + + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetManyByOrganizationIdAsync(default); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetManyByUserIdAsync(default); + } } diff --git a/test/Core.Test/Utilities/CustomRedisProcessingStrategyTests.cs b/test/Core.Test/Utilities/CustomRedisProcessingStrategyTests.cs index e5b9bd5549..10f15ca530 100644 --- a/test/Core.Test/Utilities/CustomRedisProcessingStrategyTests.cs +++ b/test/Core.Test/Utilities/CustomRedisProcessingStrategyTests.cs @@ -3,7 +3,7 @@ using Bit.Core.Settings; using Bit.Core.Utilities; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; -using Moq; +using NSubstitute; using StackExchange.Redis; using Xunit; @@ -37,14 +37,12 @@ public class CustomRedisProcessingStrategyTests #endregion - private readonly Mock _mockCounterKeyBuilder = new(); - private Mock _mockDb; + private readonly ICounterKeyBuilder _mockCounterKeyBuilder = Substitute.For(); + private IDatabase _mockDb; public CustomRedisProcessingStrategyTests() { - _mockCounterKeyBuilder - .Setup(x => - x.Build(It.IsAny(), It.IsAny())) + _mockCounterKeyBuilder.Build(Arg.Any(), Arg.Any()) .Returns(_sampleClientId.ClientId); } @@ -55,12 +53,12 @@ public class CustomRedisProcessingStrategyTests var strategy = BuildProcessingStrategy(); // Act - var result = await strategy.ProcessRequestAsync(_sampleClientId, _sampleRule, _mockCounterKeyBuilder.Object, _sampleOptions, + var result = await strategy.ProcessRequestAsync(_sampleClientId, _sampleRule, _mockCounterKeyBuilder, _sampleOptions, CancellationToken.None); // Assert Assert.Equal(1, result.Count); - VerifyRedisCalls(Times.Once()); + VerifyRedisCalls(1); } [Fact] @@ -70,60 +68,63 @@ public class CustomRedisProcessingStrategyTests var strategy = BuildProcessingStrategy(false); // Act - var result = await strategy.ProcessRequestAsync(_sampleClientId, _sampleRule, _mockCounterKeyBuilder.Object, _sampleOptions, + var result = await strategy.ProcessRequestAsync(_sampleClientId, _sampleRule, _mockCounterKeyBuilder, _sampleOptions, CancellationToken.None); // Assert Assert.Equal(0, result.Count); - VerifyRedisCalls(Times.Never()); + VerifyRedisNotCalled(); } [Fact] public async Task SkipRateLimit_When_TimeoutThresholdExceeded() { // Arrange - var mockCache = new Mock(); + var mockCache = Substitute.For(); object existingCount = new CustomRedisProcessingStrategy.TimeoutCounter { Count = _sampleSettings.DistributedIpRateLimiting.MaxRedisTimeoutsThreshold + 1 }; - mockCache.Setup(x => x.TryGetValue(It.IsAny(), out existingCount)).Returns(true); + mockCache.TryGetValue(Arg.Any(), out existingCount).ReturnsForAnyArgs(x => + { + x[1] = existingCount; + return true; + }); - var strategy = BuildProcessingStrategy(mockCache: mockCache.Object); + var strategy = BuildProcessingStrategy(mockCache: mockCache); // Act - var result = await strategy.ProcessRequestAsync(_sampleClientId, _sampleRule, _mockCounterKeyBuilder.Object, _sampleOptions, + var result = await strategy.ProcessRequestAsync(_sampleClientId, _sampleRule, _mockCounterKeyBuilder, _sampleOptions, CancellationToken.None); // Assert Assert.Equal(0, result.Count); - VerifyRedisCalls(Times.Never()); + VerifyRedisNotCalled(); } [Fact] public async Task SkipRateLimit_When_RedisTimeoutException() { // Arrange - var mockCache = new Mock(); - var mockCacheEntry = new Mock(); - mockCacheEntry.SetupAllProperties(); - mockCache.Setup(x => x.CreateEntry(It.IsAny())).Returns(mockCacheEntry.Object); + var mockCache = Substitute.For(); + var mockCacheEntry = Substitute.For(); + mockCache.CreateEntry(Arg.Any()).Returns(mockCacheEntry); - var strategy = BuildProcessingStrategy(mockCache: mockCache.Object, throwRedisTimeout: true); + var strategy = BuildProcessingStrategy(mockCache: mockCache, throwRedisTimeout: true); // Act - var result = await strategy.ProcessRequestAsync(_sampleClientId, _sampleRule, _mockCounterKeyBuilder.Object, _sampleOptions, + var result = await strategy.ProcessRequestAsync(_sampleClientId, _sampleRule, _mockCounterKeyBuilder, _sampleOptions, CancellationToken.None); - var timeoutCounter = ((CustomRedisProcessingStrategy.TimeoutCounter)mockCacheEntry.Object.Value); + var timeoutCounter = ((CustomRedisProcessingStrategy.TimeoutCounter)mockCacheEntry.Value); // Assert Assert.Equal(0, result.Count); // Skip rate limiting - VerifyRedisCalls(Times.Once()); + VerifyRedisCalls(1); Assert.Equal(1, timeoutCounter.Count); // Timeout count increased/cached - Assert.NotNull(mockCacheEntry.Object.AbsoluteExpiration); - mockCache.Verify(x => x.CreateEntry(It.IsAny())); + Assert.NotNull(mockCacheEntry.AbsoluteExpiration); + mockCache.Received().CreateEntry(Arg.Any()); } [Fact] @@ -136,26 +137,33 @@ public class CustomRedisProcessingStrategyTests // Act // Redis Timeout 1 - await strategy.ProcessRequestAsync(_sampleClientId, _sampleRule, _mockCounterKeyBuilder.Object, _sampleOptions, + await strategy.ProcessRequestAsync(_sampleClientId, _sampleRule, _mockCounterKeyBuilder, _sampleOptions, CancellationToken.None); // Redis Timeout 2 - await strategy.ProcessRequestAsync(_sampleClientId, _sampleRule, _mockCounterKeyBuilder.Object, _sampleOptions, + await strategy.ProcessRequestAsync(_sampleClientId, _sampleRule, _mockCounterKeyBuilder, _sampleOptions, CancellationToken.None); // Skip Redis - await strategy.ProcessRequestAsync(_sampleClientId, _sampleRule, _mockCounterKeyBuilder.Object, _sampleOptions, + await strategy.ProcessRequestAsync(_sampleClientId, _sampleRule, _mockCounterKeyBuilder, _sampleOptions, CancellationToken.None); // Assert - VerifyRedisCalls(Times.Exactly(_sampleSettings.DistributedIpRateLimiting.MaxRedisTimeoutsThreshold)); + VerifyRedisCalls(_sampleSettings.DistributedIpRateLimiting.MaxRedisTimeoutsThreshold); } - private void VerifyRedisCalls(Times times) + private void VerifyRedisCalls(int times) { - _mockDb.Verify(x => - x.ScriptEvaluateAsync(It.IsAny(), It.IsAny(), It.IsAny()), - times); + _mockDb + .Received(times) + .ScriptEvaluateAsync(Arg.Any(), Arg.Any(), Arg.Any()); + } + + private void VerifyRedisNotCalled() + { + _mockDb + .DidNotReceive() + .ScriptEvaluateAsync(Arg.Any(), Arg.Any(), Arg.Any()); } private CustomRedisProcessingStrategy BuildProcessingStrategy( @@ -163,36 +171,33 @@ public class CustomRedisProcessingStrategyTests bool throwRedisTimeout = false, IMemoryCache mockCache = null) { - var mockRedisConnection = new Mock(); + var mockRedisConnection = Substitute.For(); - mockRedisConnection.Setup(x => x.IsConnected).Returns(isRedisConnected); + mockRedisConnection.IsConnected.Returns(isRedisConnected); - _mockDb = new Mock(); + _mockDb = Substitute.For(); var mockScriptEvaluate = _mockDb - .Setup(x => - x.ScriptEvaluateAsync(It.IsAny(), It.IsAny(), It.IsAny())); + .ScriptEvaluateAsync(Arg.Any(), Arg.Any(), Arg.Any()); if (throwRedisTimeout) { - mockScriptEvaluate.ThrowsAsync(new RedisTimeoutException("Timeout", CommandStatus.WaitingToBeSent)); + mockScriptEvaluate.Returns(x => throw new RedisTimeoutException("Timeout", CommandStatus.WaitingToBeSent)); } else { - mockScriptEvaluate.ReturnsAsync(RedisResult.Create(1)); + mockScriptEvaluate.Returns(RedisResult.Create(1)); } - mockRedisConnection - .Setup(x => - x.GetDatabase(It.IsAny(), It.IsAny())) - .Returns(_mockDb.Object); + mockRedisConnection.GetDatabase(Arg.Any(), Arg.Any()) + .Returns(_mockDb); - var mockLogger = new Mock>(); - var mockConfig = new Mock(); + var mockLogger = Substitute.For>(); + var mockConfig = Substitute.For(); - mockCache ??= new Mock().Object; + mockCache ??= Substitute.For(); - return new CustomRedisProcessingStrategy(mockRedisConnection.Object, mockConfig.Object, - mockLogger.Object, mockCache, _sampleSettings); + return new CustomRedisProcessingStrategy(mockRedisConnection, mockConfig, + mockLogger, mockCache, _sampleSettings); } } diff --git a/test/Core.Test/packages.lock.json b/test/Core.Test/packages.lock.json index 6337c1a659..d319b95eda 100644 --- a/test/Core.Test/packages.lock.json +++ b/test/Core.Test/packages.lock.json @@ -48,16 +48,6 @@ "Microsoft.TestPlatform.TestHost": "17.1.0" } }, - "Moq": { - "type": "Direct", - "requested": "[4.17.2, )", - "resolved": "4.17.2", - "contentHash": "HytUPJ3/uks2UgJ9hIcyXm3YxpFAR4OJzbQwTHltbKGun3lFLhEHs97hiiPj1dY8jV/kasXeihTzDxct6Zf3iQ==", - "dependencies": { - "Castle.Core": "4.4.1", - "System.Threading.Tasks.Extensions": "4.5.4" - } - }, "NSubstitute": { "type": "Direct", "requested": "[4.3.0, )", @@ -2680,55 +2670,55 @@ "common": { "type": "Project", "dependencies": { - "AutoFixture.AutoNSubstitute": "4.17.0", - "AutoFixture.Xunit2": "4.17.0", - "Core": "2023.7.2", - "Kralizek.AutoFixture.Extensions.MockHttp": "1.2.0", - "Microsoft.NET.Test.Sdk": "17.1.0", - "NSubstitute": "4.3.0", - "xunit": "2.4.1" + "AutoFixture.AutoNSubstitute": "[4.17.0, )", + "AutoFixture.Xunit2": "[4.17.0, )", + "Core": "[2023.7.2, )", + "Kralizek.AutoFixture.Extensions.MockHttp": "[1.2.0, )", + "Microsoft.NET.Test.Sdk": "[17.1.0, )", + "NSubstitute": "[4.3.0, )", + "xunit": "[2.4.1, )" } }, "core": { "type": "Project", "dependencies": { - "AWSSDK.SQS": "3.7.2.47", - "AWSSDK.SimpleEmail": "3.7.0.150", - "AspNetCoreRateLimit": "4.0.2", - "AspNetCoreRateLimit.Redis": "1.0.1", - "Azure.Extensions.AspNetCore.DataProtection.Blobs": "1.3.2", - "Azure.Messaging.ServiceBus": "7.15.0", - "Azure.Storage.Blobs": "12.14.1", - "Azure.Storage.Queues": "12.12.0", - "BitPay.Light": "1.0.1907", - "Braintree": "5.12.0", - "DnsClient": "1.7.0", - "Fido2.AspNet": "3.0.1", - "Handlebars.Net": "2.1.2", - "IdentityServer4": "4.1.2", - "IdentityServer4.AccessTokenValidation": "3.0.1", - "LaunchDarkly.ServerSdk": "7.0.0", - "MailKit": "3.2.0", - "Microsoft.AspNetCore.Authentication.JwtBearer": "6.0.4", - "Microsoft.Azure.Cosmos.Table": "1.0.8", - "Microsoft.Azure.NotificationHubs": "4.1.0", - "Microsoft.Data.SqlClient": "5.0.1", - "Microsoft.Extensions.Caching.StackExchangeRedis": "6.0.6", - "Microsoft.Extensions.Configuration.EnvironmentVariables": "6.0.1", - "Microsoft.Extensions.Configuration.UserSecrets": "6.0.1", - "Microsoft.Extensions.Identity.Stores": "6.0.4", - "Newtonsoft.Json": "13.0.1", - "Otp.NET": "1.2.2", - "Quartz": "3.4.0", - "SendGrid": "9.27.0", - "Sentry.Serilog": "3.16.0", - "Serilog.AspNetCore": "5.0.0", - "Serilog.Extensions.Logging": "3.1.0", - "Serilog.Extensions.Logging.File": "2.0.0", - "Serilog.Sinks.AzureCosmosDB": "2.0.0", - "Serilog.Sinks.SyslogMessages": "2.0.6", - "Stripe.net": "40.0.0", - "YubicoDotNetClient": "1.2.0" + "AWSSDK.SQS": "[3.7.2.47, )", + "AWSSDK.SimpleEmail": "[3.7.0.150, )", + "AspNetCoreRateLimit": "[4.0.2, )", + "AspNetCoreRateLimit.Redis": "[1.0.1, )", + "Azure.Extensions.AspNetCore.DataProtection.Blobs": "[1.3.2, )", + "Azure.Messaging.ServiceBus": "[7.15.0, )", + "Azure.Storage.Blobs": "[12.14.1, )", + "Azure.Storage.Queues": "[12.12.0, )", + "BitPay.Light": "[1.0.1907, )", + "Braintree": "[5.12.0, )", + "DnsClient": "[1.7.0, )", + "Fido2.AspNet": "[3.0.1, )", + "Handlebars.Net": "[2.1.2, )", + "IdentityServer4": "[4.1.2, )", + "IdentityServer4.AccessTokenValidation": "[3.0.1, )", + "LaunchDarkly.ServerSdk": "[7.0.0, )", + "MailKit": "[3.2.0, )", + "Microsoft.AspNetCore.Authentication.JwtBearer": "[6.0.4, )", + "Microsoft.Azure.Cosmos.Table": "[1.0.8, )", + "Microsoft.Azure.NotificationHubs": "[4.1.0, )", + "Microsoft.Data.SqlClient": "[5.0.1, )", + "Microsoft.Extensions.Caching.StackExchangeRedis": "[6.0.6, )", + "Microsoft.Extensions.Configuration.EnvironmentVariables": "[6.0.1, )", + "Microsoft.Extensions.Configuration.UserSecrets": "[6.0.1, )", + "Microsoft.Extensions.Identity.Stores": "[6.0.4, )", + "Newtonsoft.Json": "[13.0.1, )", + "Otp.NET": "[1.2.2, )", + "Quartz": "[3.4.0, )", + "SendGrid": "[9.27.0, )", + "Sentry.Serilog": "[3.16.0, )", + "Serilog.AspNetCore": "[5.0.0, )", + "Serilog.Extensions.Logging": "[3.1.0, )", + "Serilog.Extensions.Logging.File": "[2.0.0, )", + "Serilog.Sinks.AzureCosmosDB": "[2.0.0, )", + "Serilog.Sinks.SyslogMessages": "[2.0.6, )", + "Stripe.net": "[40.0.0, )", + "YubicoDotNetClient": "[1.2.0, )" } } } diff --git a/test/Icons.Test/Icons.Test.csproj b/test/Icons.Test/Icons.Test.csproj index 13cfb00987..26f1913451 100644 --- a/test/Icons.Test/Icons.Test.csproj +++ b/test/Icons.Test/Icons.Test.csproj @@ -20,6 +20,7 @@ + diff --git a/test/Icons.Test/Models/IconHttpRequestTests.cs b/test/Icons.Test/Models/IconHttpRequestTests.cs new file mode 100644 index 0000000000..89e0e37eeb --- /dev/null +++ b/test/Icons.Test/Models/IconHttpRequestTests.cs @@ -0,0 +1,38 @@ +using System.Net; +using Bit.Icons.Models; +using Bit.Icons.Services; +using Bit.Test.Common.MockedHttpClient; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Net.Http.Headers; +using NSubstitute; +using Xunit; + +namespace Bit.Icons.Test.Models; + +public class IconHttpRequestTests +{ + [Fact] + public async Task FetchAsync_FollowsTwoRedirectsAsync() + { + var handler = new MockedHttpMessageHandler(); + + var request = handler + .Fallback + .WithStatusCode(HttpStatusCode.Redirect) + .WithContent("text/html", "Redirect 2Redirect 3") + .WithHeader(HeaderNames.Location, "https://icon.test"); + + var clientFactory = Substitute.For(); + clientFactory.CreateClient("Icons").Returns(handler.ToHttpClient()); + + var uriService = Substitute.For(); + uriService.TryGetUri(Arg.Any(), out Arg.Any()).Returns(x => + { + x[1] = new IconUri(new Uri("https://icon.test"), IPAddress.Parse("192.0.2.1")); + return true; + }); + var result = await IconHttpRequest.FetchAsync(new Uri("https://icon.test"), NullLogger.Instance, clientFactory, uriService); + + Assert.Equal(3, request.NumberOfResponses); // Initial + 2 redirects + } +} diff --git a/test/Icons.Test/Models/IconHttpResponseTests.cs b/test/Icons.Test/Models/IconHttpResponseTests.cs new file mode 100644 index 0000000000..d6c792e2e7 --- /dev/null +++ b/test/Icons.Test/Models/IconHttpResponseTests.cs @@ -0,0 +1,101 @@ +using System.Net; +using AngleSharp.Html.Parser; +using Bit.Icons.Models; +using Bit.Icons.Services; +using Bit.Test.Common.Helpers; +using Bit.Test.Common.MockedHttpClient; +using Microsoft.Extensions.Logging.Abstractions; +using NSubstitute; +using Xunit; + +namespace Bit.Icons.Test.Models; + +public class IconHttpResponseTests +{ + private readonly IUriService _mockedUriService; + private static readonly IHtmlParser _parser = new HtmlParser(); + + public IconHttpResponseTests() + { + _mockedUriService = Substitute.For(); + _mockedUriService.TryGetUri(Arg.Any(), out Arg.Any()).Returns(x => + { + x[1] = new IconUri(new Uri("https://icon.test"), IPAddress.Parse("192.0.2.1")); + return true; + }); + } + + [Fact] + public async Task RetrieveIconsAsync_Processes200LinksAsync() + { + var htmlBuilder = new HtmlBuilder(); + var headBuilder = new HtmlBuilder("head"); + for (var i = 0; i < 200; i++) + { + headBuilder.Append(UnusableLinkNode()); + } + headBuilder.Append(UsableLinkNode()); + htmlBuilder.Append(headBuilder); + var response = GetHttpResponseMessage(htmlBuilder.ToString()); + var sut = CurriedIconHttpResponse()(response); + + var result = await sut.RetrieveIconsAsync(new Uri("https://icon.test"), "icon.test", _parser); + + Assert.Empty(result); + } + + [Fact] + public async Task RetrieveIconsAsync_Processes10IconsAsync() + { + var htmlBuilder = new HtmlBuilder(); + var headBuilder = new HtmlBuilder("head"); + for (var i = 0; i < 11; i++) + { + headBuilder.Append(UsableLinkNode()); + } + htmlBuilder.Append(headBuilder); + var response = GetHttpResponseMessage(htmlBuilder.ToString()); + var sut = CurriedIconHttpResponse()(response); + + var result = await sut.RetrieveIconsAsync(new Uri("https://icon.test"), "icon.test", _parser); + + Assert.Equal(10, result.Count()); + } + + private static string UsableLinkNode() + { + return ""; + } + + private static string UnusableLinkNode() + { + // Empty href links are not usable + return ""; + } + + private static HttpResponseMessage GetHttpResponseMessage(string content) + { + return new HttpResponseMessage(HttpStatusCode.OK) + { + RequestMessage = new HttpRequestMessage(HttpMethod.Get, "https://icon.test"), + Content = new StringContent(content) + }; + } + + private Func CurriedIconHttpResponse() + { + return (HttpResponseMessage response) => new IconHttpResponse(response, NullLogger.Instance, UsableIconHttpClientFactory(), _mockedUriService); + } + + private static IHttpClientFactory UsableIconHttpClientFactory() + { + var substitute = Substitute.For(); + var handler = new MockedHttpMessageHandler(); + handler.Fallback + .WithStatusCode(HttpStatusCode.OK) + .WithContent("image/png", new byte[] { 137, 80, 78, 71 }); + + substitute.CreateClient("Icons").Returns(handler.ToHttpClient()); + return substitute; + } +} diff --git a/test/Icons.Test/Models/IconLinkTests.cs b/test/Icons.Test/Models/IconLinkTests.cs new file mode 100644 index 0000000000..db4399670c --- /dev/null +++ b/test/Icons.Test/Models/IconLinkTests.cs @@ -0,0 +1,85 @@ +using System.Net; +using AngleSharp.Dom; +using Bit.Icons.Models; +using Bit.Icons.Services; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Xunit; + +namespace Bit.Icons.Test.Models; + +public class IconLinkTests +{ + private readonly IElement _element; + private readonly Uri _uri = new("https://icon.test"); + private readonly ILogger _logger = Substitute.For>(); + private readonly IHttpClientFactory _httpClientFactory; + private readonly IUriService _uriService; + private readonly string _baseUrlPath = "/"; + + public IconLinkTests() + { + _element = Substitute.For(); + _httpClientFactory = Substitute.For(); + _uriService = Substitute.For(); + _uriService.TryGetUri(Arg.Any(), out Arg.Any()).Returns(x => + { + x[1] = new IconUri(new Uri("https://icon.test"), IPAddress.Parse("192.0.2.1")); + return true; + }); + } + + [Fact] + public void WithNoHref_IsNotUsable() + { + _element.GetAttribute("href").Returns(string.Empty); + + var result = new IconLink(_element, _uri, _baseUrlPath).IsUsable(); + + Assert.False(result); + } + + [Theory] + [InlineData(null, false)] + [InlineData("", false)] + [InlineData(" ", false)] + [InlineData("unusable", false)] + [InlineData("ico", true)] + public void WithNoRel_IsUsable(string extension, bool expectedResult) + { + SetAttributeValue("href", $"/favicon.{extension}"); + + var result = new IconLink(_element, _uri, _baseUrlPath).IsUsable(); + + Assert.Equal(expectedResult, result); + } + + [Theory] + [InlineData("icon", true)] + [InlineData("stylesheet", false)] + public void WithRel_IsUsable(string rel, bool expectedResult) + { + SetAttributeValue("href", "/favicon.ico"); + SetAttributeValue("rel", rel); + + var result = new IconLink(_element, _uri, _baseUrlPath).IsUsable(); + + Assert.Equal(expectedResult, result); + } + + [Fact] + public void FetchAsync_Unvalidated_ReturnsNull() + { + var result = new IconLink(_element, _uri, _baseUrlPath).FetchAsync(_logger, _httpClientFactory, _uriService); + + Assert.Null(result.Result); + } + + private void SetAttributeValue(string attribute, string value) + { + var attr = Substitute.For(); + attr.Value.Returns(value); + + _element.Attributes[attribute].Returns(attr); + } +} diff --git a/test/Icons.Test/Models/IconUriTests.cs b/test/Icons.Test/Models/IconUriTests.cs new file mode 100644 index 0000000000..8363fc9bb9 --- /dev/null +++ b/test/Icons.Test/Models/IconUriTests.cs @@ -0,0 +1,22 @@ +using System.Net; +using Bit.Icons.Models; +using Xunit; + +namespace Bit.Icons.Test.Models; + +public class IconUriTests +{ + [Theory] + [InlineData("https://icon.test", "1.1.1.1", true)] + [InlineData("https://icon.test:4443", "1.1.1.1", false)] // Non standard port + [InlineData("http://test", "1.1.1.1", false)] // top level domain + [InlineData("https://icon.test", "127.0.0.1", false)] // IP is internal + [InlineData("https://icon.test", "::1", false)] // IP is internal + [InlineData("https://1.1.1.1", "::1", false)] // host is IP + public void IsValid(string uri, string ip, bool expectedResult) + { + var result = new IconUri(new Uri(uri), IPAddress.Parse(ip)).IsValid; + + Assert.Equal(expectedResult, result); + } +} diff --git a/test/Icons.Test/Services/IconFetchingServiceTests.cs b/test/Icons.Test/Services/IconFetchingServiceTests.cs index 59f25af244..ad73a2cfc9 100644 --- a/test/Icons.Test/Services/IconFetchingServiceTests.cs +++ b/test/Icons.Test/Services/IconFetchingServiceTests.cs @@ -1,25 +1,25 @@ using Bit.Icons.Services; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using Xunit; namespace Bit.Icons.Test.Services; -public class IconFetchingServiceTests +public class IconFetchingServiceTests : ServiceTestBase { [Theory] + [InlineData("www.twitter.com")] // https site [InlineData("www.google.com")] // https site [InlineData("neverssl.com")] // http site - [InlineData("ameritrade.com")] + [InlineData("neopets.com")] // uses favicon.ico + [InlineData("hopin.com")] // uses svg+xml format + [InlineData("ameritrade.com")] // redirects to tdameritrace.com [InlineData("icloud.com")] [InlineData("bofa.com", Skip = "Broken in pipeline for .NET 6. Tracking link: https://bitwarden.atlassian.net/browse/PS-982")] public async Task GetIconAsync_Success(string domain) { - var sut = new IconFetchingService(GetLogger()); + var sut = BuildSut(); var result = await sut.GetIconAsync(domain); Assert.NotNull(result); - Assert.NotNull(result.Icon); } [Theory] @@ -28,23 +28,12 @@ public class IconFetchingServiceTests [InlineData("localhost")] public async Task GetIconAsync_ReturnsNull(string domain) { - var sut = new IconFetchingService(GetLogger()); + var sut = BuildSut(); var result = await sut.GetIconAsync(domain); Assert.Null(result); } - private static ILogger GetLogger() - { - var services = new ServiceCollection(); - services.AddLogging(b => - { - b.ClearProviders(); - b.AddDebug(); - }); - - var provider = services.BuildServiceProvider(); - - return provider.GetRequiredService>(); - } + private IconFetchingService BuildSut() => + GetService(); } diff --git a/test/Icons.Test/Services/ServiceTestBase.cs b/test/Icons.Test/Services/ServiceTestBase.cs new file mode 100644 index 0000000000..37d816972e --- /dev/null +++ b/test/Icons.Test/Services/ServiceTestBase.cs @@ -0,0 +1,41 @@ +using Bit.Icons.Extensions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Bit.Icons.Test.Services; + +public class ServiceTestBase +{ + internal ServiceCollection _services = new(); + internal ServiceProvider _provider; + + public ServiceTestBase() + { + _services = new ServiceCollection(); + _services.AddLogging(b => + { + b.ClearProviders(); + b.AddDebug(); + }); + + _services.ConfigureHttpClients(); + _services.AddHtmlParsing(); + _services.AddServices(); + + _provider = _services.BuildServiceProvider(); + } + + public T GetService() => + _provider.GetRequiredService(); +} + +public class ServiceTestBase : ServiceTestBase where TSut : class +{ + public ServiceTestBase() : base() + { + _services.AddTransient(); + _provider = _services.BuildServiceProvider(); + } + + public TSut Sut => GetService(); +} diff --git a/test/Icons.Test/packages.lock.json b/test/Icons.Test/packages.lock.json index 689c3c09eb..4bdc813b6c 100644 --- a/test/Icons.Test/packages.lock.json +++ b/test/Icons.Test/packages.lock.json @@ -46,11 +46,10 @@ }, "AngleSharp": { "type": "Transitive", - "resolved": "0.16.1", - "contentHash": "1k7Vbfmr5IUsGaR0QJwTe8XF9zacFUIoWxMgI4X/ipiyKxCWZJZoaG96fNEugL90iubvboRvE1IxuBPibET/Rg==", + "resolved": "1.0.4", + "contentHash": "G8R4C2MEDFQvjUbYz1QIcGfibjsTJnzP0aWy8iQgWWk7eKacYydCNGD3JMhVL0Q5pZQ9RYlqpKNESEU5NpqsRw==", "dependencies": { - "System.Buffers": "4.5.1", - "System.Text.Encoding.CodePages": "5.0.0" + "System.Text.Encoding.CodePages": "6.0.0" } }, "AspNetCoreRateLimit": { @@ -73,6 +72,33 @@ "StackExchange.Redis": "2.5.43" } }, + "AutoFixture": { + "type": "Transitive", + "resolved": "4.17.0", + "contentHash": "efMRCG3Epc4QDELwdmQGf6/caQUleRXPRCnLAq5gLMpTuOTcOQWV12vEJ8qo678Rj97/TjjxHYu/34rGkXdVAA==", + "dependencies": { + "Fare": "[2.1.1, 3.0.0)", + "System.ComponentModel.Annotations": "4.3.0" + } + }, + "AutoFixture.AutoNSubstitute": { + "type": "Transitive", + "resolved": "4.17.0", + "contentHash": "iWsRiDQ7T8s6F4mvYbSvPTq0GDtxJD6D+E1Fu9gVbHUvJiNikC1yIDNTH+3tQF7RK864HH/3R8ETj9m2X8UXvg==", + "dependencies": { + "AutoFixture": "4.17.0", + "NSubstitute": "[2.0.3, 5.0.0)" + } + }, + "AutoFixture.Xunit2": { + "type": "Transitive", + "resolved": "4.17.0", + "contentHash": "lrURL/LhJLPkn2tSPUEW8Wscr5LoV2Mr8A+ikn5gwkofex3o7qWUsBswlLw+KCA7EOpeqwZOldp3k91zDF+48Q==", + "dependencies": { + "AutoFixture": "4.17.0", + "xunit.extensibility.core": "[2.2.0, 3.0.0)" + } + }, "AutoMapper": { "type": "Transitive", "resolved": "12.0.1", @@ -246,6 +272,14 @@ "Microsoft.Win32.Registry": "5.0.0" } }, + "Fare": { + "type": "Transitive", + "resolved": "2.1.1", + "contentHash": "HaI8puqA66YU7/9cK4Sgbs1taUTP1Ssa4QT2PIzqJ7GvAbN1QgkjbRsjH+FSbMh1MJdvS0CIwQNLtFT+KF6KpA==", + "dependencies": { + "NETStandard.Library": "1.6.1" + } + }, "Fido2": { "type": "Transitive", "resolved": "3.0.1", @@ -326,6 +360,15 @@ "IdentityModel": "4.4.0" } }, + "Kralizek.AutoFixture.Extensions.MockHttp": { + "type": "Transitive", + "resolved": "1.2.0", + "contentHash": "6zmks7/5mVczazv910N7V2EdiU6B+rY61lwdgVO0o2iZuTI6KI3T+Hgkrjv0eGOKYucq2OMC+gnAc5Ej2ajoTQ==", + "dependencies": { + "AutoFixture": "4.11.0", + "RichardSzalay.MockHttp": "6.0.0" + } + }, "LaunchDarkly.Cache": { "type": "Transitive", "resolved": "1.0.2", @@ -1148,6 +1191,11 @@ "System.Diagnostics.DiagnosticSource": "4.7.1" } }, + "RichardSzalay.MockHttp": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "bStGNqIX/MGYtML7K3EzdsE/k5HGVAcg7XgN23TQXGXqxNC9fvYFR94fA0sGM5hAT36R+BBGet6ZDQxXL/IPxg==" + }, "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { "type": "Transitive", "resolved": "4.3.2", @@ -1568,6 +1616,24 @@ "System.Runtime": "4.3.0" } }, + "System.ComponentModel.Annotations": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "SY2RLItHt43rd8J9D8M8e8NM4m+9WLN2uUd9G0n1I4hj/7w+v3pzK6ZBjexlG1/2xvLKQsqir3UGVSyBTXMLWA==", + "dependencies": { + "System.Collections": "4.3.0", + "System.ComponentModel": "4.3.0", + "System.Globalization": "4.3.0", + "System.Linq": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Text.RegularExpressions": "4.3.0", + "System.Threading": "4.3.0" + } + }, "System.ComponentModel.Primitives": { "type": "Transitive", "resolved": "4.3.0", @@ -2509,10 +2575,10 @@ }, "System.Text.Encoding.CodePages": { "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "NyscU59xX6Uo91qvhOs2Ccho3AR2TnZPomo1Z0K6YpyztBPM/A5VbkzOO19sy3A3i1TtEnTxA7bCe3Us+r5MWg==", + "resolved": "6.0.0", + "contentHash": "ZFCILZuOvtKPauZ/j/swhvw68ZRi9ATCfvGbk1QfydmcXBkIWecWKn/250UH7rahZ5OoDBaiAudJtPvLwzw85A==", "dependencies": { - "Microsoft.NETCore.Platforms": "5.0.0" + "System.Runtime.CompilerServices.Unsafe": "6.0.0" } }, "System.Text.Encoding.Extensions": { @@ -2770,6 +2836,18 @@ "NETStandard.Library": "1.6.1" } }, + "common": { + "type": "Project", + "dependencies": { + "AutoFixture.AutoNSubstitute": "[4.17.0, )", + "AutoFixture.Xunit2": "[4.17.0, )", + "Core": "[2023.7.2, )", + "Kralizek.AutoFixture.Extensions.MockHttp": "[1.2.0, )", + "Microsoft.NET.Test.Sdk": "[17.1.0, )", + "NSubstitute": "[4.3.0, )", + "xunit": "[2.4.1, )" + } + }, "core": { "type": "Project", "dependencies": { @@ -2815,7 +2893,7 @@ "icons": { "type": "Project", "dependencies": { - "AngleSharp": "0.16.1", + "AngleSharp": "1.0.4", "Core": "2023.7.2", "SharedWeb": "2023.7.2" } diff --git a/test/Identity.IntegrationTest/Endpoints/IdentityServerTests.cs b/test/Identity.IntegrationTest/Endpoints/IdentityServerTests.cs index f29a09c6e2..b1e74bd17d 100644 --- a/test/Identity.IntegrationTest/Endpoints/IdentityServerTests.cs +++ b/test/Identity.IntegrationTest/Endpoints/IdentityServerTests.cs @@ -33,7 +33,7 @@ public class IdentityServerTests : IClassFixture using var body = await AssertHelper.AssertResponseTypeIs(context); var endpointRoot = body.RootElement; - // WARNING: Edits to this file should NOT just be made to "get the test to work" they should be made when intentional + // WARNING: Edits to this file should NOT just be made to "get the test to work" they should be made when intentional // changes were made to this endpoint and proper testing will take place to ensure clients are backwards compatible // or loss of functionality is properly noted. await using var fs = File.OpenRead("openid-configuration.json"); @@ -372,10 +372,10 @@ public class IdentityServerTests : IClassFixture } /// - /// This test currently does not test any code that is not covered by other tests but + /// This test currently does not test any code that is not covered by other tests but /// it shows that we probably have some dead code in /// for installation, organization, and user they split on a '.' but have already checked that at least one - /// '.' exists in the client_id by checking it with + /// '.' exists in the client_id by checking it with /// I believe that idParts.Length > 1 will ALWAYS return true /// [Fact] @@ -488,9 +488,9 @@ public class IdentityServerTests : IClassFixture } [Theory, BitAutoData] - public async Task TokenEndpoint_ToQuickInOneSecond_BlockRequest(string deviceId) + public async Task TokenEndpoint_TooQuickInOneSecond_BlockRequest(string deviceId) { - const int AmountInOneSecondAllowed = 5; + const int AmountInOneSecondAllowed = 10; // The rule we are testing is 10 requests in 1 second var username = "test+ratelimiting@email.com"; @@ -514,9 +514,9 @@ public class IdentityServerTests : IClassFixture } var responses = (await Task.WhenAll(tasks)).ToList(); + var blockResponses = responses.Where(c => c.Response.StatusCode == StatusCodes.Status429TooManyRequests); - Assert.Equal(5, responses.Count(c => c.Response.StatusCode == StatusCodes.Status200OK)); - Assert.Equal(1, responses.Count(c => c.Response.StatusCode == StatusCodes.Status429TooManyRequests)); + Assert.True(blockResponses.Count() > 0); Task MakeRequest() { diff --git a/test/Infrastructure.EFIntegration.Test/AutoFixture/EntityFrameworkRepositoryFixtures.cs b/test/Infrastructure.EFIntegration.Test/AutoFixture/EntityFrameworkRepositoryFixtures.cs index c1325af216..fd14cde65b 100644 --- a/test/Infrastructure.EFIntegration.Test/AutoFixture/EntityFrameworkRepositoryFixtures.cs +++ b/test/Infrastructure.EFIntegration.Test/AutoFixture/EntityFrameworkRepositoryFixtures.cs @@ -10,7 +10,7 @@ using Bit.Infrastructure.EntityFramework.Repositories; using Bit.Infrastructure.EntityFramework.Vault.Models; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; -using Moq; +using NSubstitute; namespace Bit.Infrastructure.EFIntegration.Test.AutoFixture; @@ -25,20 +25,16 @@ internal class ServiceScopeFactoryBuilder : ISpecimenBuilder public object Create(object request, ISpecimenContext context) { var fixture = new Fixture(); - var serviceProvider = new Mock(); + var serviceProvider = Substitute.For(); var dbContext = new DatabaseContext(_options); - serviceProvider - .Setup(x => x.GetService(typeof(DatabaseContext))) - .Returns(dbContext); + serviceProvider.GetService(typeof(DatabaseContext)).Returns(dbContext); - var serviceScope = new Mock(); - serviceScope.Setup(x => x.ServiceProvider).Returns(serviceProvider.Object); + var serviceScope = Substitute.For(); + serviceScope.ServiceProvider.Returns(serviceProvider); - var serviceScopeFactory = new Mock(); - serviceScopeFactory - .Setup(x => x.CreateScope()) - .Returns(serviceScope.Object); - return serviceScopeFactory.Object; + var serviceScopeFactory = Substitute.For(); + serviceScopeFactory.CreateScope().Returns(serviceScope); + return serviceScopeFactory; } } diff --git a/test/Infrastructure.EFIntegration.Test/Helpers/DatabaseOptionsFactory.cs b/test/Infrastructure.EFIntegration.Test/Helpers/DatabaseOptionsFactory.cs index 4ee68aa8ee..cc1dba46c1 100644 --- a/test/Infrastructure.EFIntegration.Test/Helpers/DatabaseOptionsFactory.cs +++ b/test/Infrastructure.EFIntegration.Test/Helpers/DatabaseOptionsFactory.cs @@ -5,7 +5,7 @@ using Bit.Infrastructure.EntityFramework.Repositories; using Microsoft.AspNetCore.DataProtection; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; -using Moq; +using NSubstitute; namespace Bit.Infrastructure.EFIntegration.Test.Helpers; @@ -18,17 +18,17 @@ public static class DatabaseOptionsFactory var services = new ServiceCollection() .AddSingleton(sp => { - var dataProtector = new Mock(); - dataProtector - .Setup(d => d.Unprotect(It.IsAny())) - .Returns(data => Encoding.UTF8.GetBytes(Constants.DatabaseFieldProtectedPrefix + Encoding.UTF8.GetString(data))); + var dataProtector = Substitute.For(); + dataProtector.Unprotect(Arg.Any()) + .Returns(data => + Encoding.UTF8.GetBytes(Constants.DatabaseFieldProtectedPrefix + + Encoding.UTF8.GetString((byte[])data[0]))); - var dataProtectionProvider = new Mock(); - dataProtectionProvider - .Setup(x => x.CreateProtector(Constants.DatabaseFieldProtectorPurpose)) - .Returns(dataProtector.Object); + var dataProtectionProvider = Substitute.For(); + dataProtectionProvider.CreateProtector(Constants.DatabaseFieldProtectorPurpose) + .Returns(dataProtector); - return dataProtectionProvider.Object; + return dataProtectionProvider; }) .BuildServiceProvider(); diff --git a/test/Infrastructure.EFIntegration.Test/Infrastructure.EFIntegration.Test.csproj b/test/Infrastructure.EFIntegration.Test/Infrastructure.EFIntegration.Test.csproj index 352e8b6824..9ce0eb7552 100644 --- a/test/Infrastructure.EFIntegration.Test/Infrastructure.EFIntegration.Test.csproj +++ b/test/Infrastructure.EFIntegration.Test/Infrastructure.EFIntegration.Test.csproj @@ -8,7 +8,6 @@ all - diff --git a/test/Infrastructure.EFIntegration.Test/packages.lock.json b/test/Infrastructure.EFIntegration.Test/packages.lock.json index a17d032f3d..377737a92f 100644 --- a/test/Infrastructure.EFIntegration.Test/packages.lock.json +++ b/test/Infrastructure.EFIntegration.Test/packages.lock.json @@ -38,16 +38,6 @@ "Microsoft.TestPlatform.TestHost": "17.1.0" } }, - "Moq": { - "type": "Direct", - "requested": "[4.17.2, )", - "resolved": "4.17.2", - "contentHash": "HytUPJ3/uks2UgJ9hIcyXm3YxpFAR4OJzbQwTHltbKGun3lFLhEHs97hiiPj1dY8jV/kasXeihTzDxct6Zf3iQ==", - "dependencies": { - "Castle.Core": "4.4.1", - "System.Threading.Tasks.Extensions": "4.5.4" - } - }, "NSubstitute": { "type": "Direct", "requested": "[4.3.0, )", @@ -2843,89 +2833,88 @@ "common": { "type": "Project", "dependencies": { - "AutoFixture.AutoNSubstitute": "4.17.0", - "AutoFixture.Xunit2": "4.17.0", - "Core": "2023.7.2", - "Kralizek.AutoFixture.Extensions.MockHttp": "1.2.0", - "Microsoft.NET.Test.Sdk": "17.1.0", - "NSubstitute": "4.3.0", - "xunit": "2.4.1" + "AutoFixture.AutoNSubstitute": "[4.17.0, )", + "AutoFixture.Xunit2": "[4.17.0, )", + "Core": "[2023.7.2, )", + "Kralizek.AutoFixture.Extensions.MockHttp": "[1.2.0, )", + "Microsoft.NET.Test.Sdk": "[17.1.0, )", + "NSubstitute": "[4.3.0, )", + "xunit": "[2.4.1, )" } }, "core": { "type": "Project", "dependencies": { - "AWSSDK.SQS": "3.7.2.47", - "AWSSDK.SimpleEmail": "3.7.0.150", - "AspNetCoreRateLimit": "4.0.2", - "AspNetCoreRateLimit.Redis": "1.0.1", - "Azure.Extensions.AspNetCore.DataProtection.Blobs": "1.3.2", - "Azure.Messaging.ServiceBus": "7.15.0", - "Azure.Storage.Blobs": "12.14.1", - "Azure.Storage.Queues": "12.12.0", - "BitPay.Light": "1.0.1907", - "Braintree": "5.12.0", - "DnsClient": "1.7.0", - "Fido2.AspNet": "3.0.1", - "Handlebars.Net": "2.1.2", - "IdentityServer4": "4.1.2", - "IdentityServer4.AccessTokenValidation": "3.0.1", - "LaunchDarkly.ServerSdk": "7.0.0", - "MailKit": "3.2.0", - "Microsoft.AspNetCore.Authentication.JwtBearer": "6.0.4", - "Microsoft.Azure.Cosmos.Table": "1.0.8", - "Microsoft.Azure.NotificationHubs": "4.1.0", - "Microsoft.Data.SqlClient": "5.0.1", - "Microsoft.Extensions.Caching.StackExchangeRedis": "6.0.6", - "Microsoft.Extensions.Configuration.EnvironmentVariables": "6.0.1", - "Microsoft.Extensions.Configuration.UserSecrets": "6.0.1", - "Microsoft.Extensions.Identity.Stores": "6.0.4", - "Newtonsoft.Json": "13.0.1", - "Otp.NET": "1.2.2", - "Quartz": "3.4.0", - "SendGrid": "9.27.0", - "Sentry.Serilog": "3.16.0", - "Serilog.AspNetCore": "5.0.0", - "Serilog.Extensions.Logging": "3.1.0", - "Serilog.Extensions.Logging.File": "2.0.0", - "Serilog.Sinks.AzureCosmosDB": "2.0.0", - "Serilog.Sinks.SyslogMessages": "2.0.6", - "Stripe.net": "40.0.0", - "YubicoDotNetClient": "1.2.0" + "AWSSDK.SQS": "[3.7.2.47, )", + "AWSSDK.SimpleEmail": "[3.7.0.150, )", + "AspNetCoreRateLimit": "[4.0.2, )", + "AspNetCoreRateLimit.Redis": "[1.0.1, )", + "Azure.Extensions.AspNetCore.DataProtection.Blobs": "[1.3.2, )", + "Azure.Messaging.ServiceBus": "[7.15.0, )", + "Azure.Storage.Blobs": "[12.14.1, )", + "Azure.Storage.Queues": "[12.12.0, )", + "BitPay.Light": "[1.0.1907, )", + "Braintree": "[5.12.0, )", + "DnsClient": "[1.7.0, )", + "Fido2.AspNet": "[3.0.1, )", + "Handlebars.Net": "[2.1.2, )", + "IdentityServer4": "[4.1.2, )", + "IdentityServer4.AccessTokenValidation": "[3.0.1, )", + "LaunchDarkly.ServerSdk": "[7.0.0, )", + "MailKit": "[3.2.0, )", + "Microsoft.AspNetCore.Authentication.JwtBearer": "[6.0.4, )", + "Microsoft.Azure.Cosmos.Table": "[1.0.8, )", + "Microsoft.Azure.NotificationHubs": "[4.1.0, )", + "Microsoft.Data.SqlClient": "[5.0.1, )", + "Microsoft.Extensions.Caching.StackExchangeRedis": "[6.0.6, )", + "Microsoft.Extensions.Configuration.EnvironmentVariables": "[6.0.1, )", + "Microsoft.Extensions.Configuration.UserSecrets": "[6.0.1, )", + "Microsoft.Extensions.Identity.Stores": "[6.0.4, )", + "Newtonsoft.Json": "[13.0.1, )", + "Otp.NET": "[1.2.2, )", + "Quartz": "[3.4.0, )", + "SendGrid": "[9.27.0, )", + "Sentry.Serilog": "[3.16.0, )", + "Serilog.AspNetCore": "[5.0.0, )", + "Serilog.Extensions.Logging": "[3.1.0, )", + "Serilog.Extensions.Logging.File": "[2.0.0, )", + "Serilog.Sinks.AzureCosmosDB": "[2.0.0, )", + "Serilog.Sinks.SyslogMessages": "[2.0.6, )", + "Stripe.net": "[40.0.0, )", + "YubicoDotNetClient": "[1.2.0, )" } }, "core.test": { "type": "Project", "dependencies": { - "AutoFixture.AutoNSubstitute": "4.17.0", - "AutoFixture.Xunit2": "4.17.0", - "Common": "2023.7.2", - "Core": "2023.7.2", - "Kralizek.AutoFixture.Extensions.MockHttp": "1.2.0", - "Microsoft.NET.Test.Sdk": "17.1.0", - "Moq": "4.17.2", - "NSubstitute": "4.3.0", - "xunit": "2.4.1" + "AutoFixture.AutoNSubstitute": "[4.17.0, )", + "AutoFixture.Xunit2": "[4.17.0, )", + "Common": "[2023.7.2, )", + "Core": "[2023.7.2, )", + "Kralizek.AutoFixture.Extensions.MockHttp": "[1.2.0, )", + "Microsoft.NET.Test.Sdk": "[17.1.0, )", + "NSubstitute": "[4.3.0, )", + "xunit": "[2.4.1, )" } }, "infrastructure.dapper": { "type": "Project", "dependencies": { - "Core": "2023.7.2", - "Dapper": "2.0.123" + "Core": "[2023.7.2, )", + "Dapper": "[2.0.123, )" } }, "infrastructure.entityframework": { "type": "Project", "dependencies": { - "AutoMapper.Extensions.Microsoft.DependencyInjection": "12.0.1", - "Core": "2023.7.2", - "Microsoft.EntityFrameworkCore.Relational": "7.0.5", - "Microsoft.EntityFrameworkCore.SqlServer": "7.0.5", - "Microsoft.EntityFrameworkCore.Sqlite": "7.0.5", - "Npgsql.EntityFrameworkCore.PostgreSQL": "7.0.4", - "Pomelo.EntityFrameworkCore.MySql": "7.0.0", - "linq2db.EntityFrameworkCore": "7.5.0" + "AutoMapper.Extensions.Microsoft.DependencyInjection": "[12.0.1, )", + "Core": "[2023.7.2, )", + "Microsoft.EntityFrameworkCore.Relational": "[7.0.5, )", + "Microsoft.EntityFrameworkCore.SqlServer": "[7.0.5, )", + "Microsoft.EntityFrameworkCore.Sqlite": "[7.0.5, )", + "Npgsql.EntityFrameworkCore.PostgreSQL": "[7.0.4, )", + "Pomelo.EntityFrameworkCore.MySql": "[7.0.0, )", + "linq2db.EntityFrameworkCore": "[7.5.0, )" } } } diff --git a/util/Attachments/Dockerfile b/util/Attachments/Dockerfile index c3a31f4f87..230de224e9 100644 --- a/util/Attachments/Dockerfile +++ b/util/Attachments/Dockerfile @@ -1,4 +1,4 @@ -FROM bitwarden/server:dev +FROM bitwardenprod.azurecr.io/server:latest LABEL com.bitwarden.product="bitwarden"