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($"{_topLevelNode}>");
+ 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
-
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"