mirror of
https://github.com/bitwarden/server.git
synced 2025-07-02 00:22:50 -05:00
Merge branch 'master' into feature/flexible-collections
This commit is contained in:
177
.github/workflows/build.yml
vendored
177
.github/workflows/build.yml
vendored
@ -10,6 +10,9 @@ on:
|
|||||||
- ".github/workflows/**"
|
- ".github/workflows/**"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
_AZ_REGISTRY: "bitwardenprod.azurecr.io"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
cloc:
|
cloc:
|
||||||
name: CLOC
|
name: CLOC
|
||||||
@ -33,6 +36,9 @@ jobs:
|
|||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||||
|
|
||||||
|
- name: Set up dotnet
|
||||||
|
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
|
||||||
|
|
||||||
- name: Verify Format
|
- name: Verify Format
|
||||||
run: dotnet format --verify-no-changes
|
run: dotnet format --verify-no-changes
|
||||||
|
|
||||||
@ -42,10 +48,11 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages
|
NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages
|
||||||
steps:
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||||
|
|
||||||
- name: Set up dotnet
|
- name: Set up dotnet
|
||||||
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
|
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
|
||||||
with:
|
|
||||||
dotnet-version: "6.0.x"
|
|
||||||
|
|
||||||
- name: Print environment
|
- name: Print environment
|
||||||
run: |
|
run: |
|
||||||
@ -54,9 +61,6 @@ jobs:
|
|||||||
echo "GitHub ref: $GITHUB_REF"
|
echo "GitHub ref: $GITHUB_REF"
|
||||||
echo "GitHub event: $GITHUB_EVENT"
|
echo "GitHub event: $GITHUB_EVENT"
|
||||||
|
|
||||||
- name: Checkout repo
|
|
||||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
|
||||||
|
|
||||||
- name: Restore
|
- name: Restore
|
||||||
run: dotnet restore --locked-mode
|
run: dotnet restore --locked-mode
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
@ -114,8 +118,14 @@ jobs:
|
|||||||
base_path: ./src
|
base_path: ./src
|
||||||
- project_name: Identity
|
- project_name: Identity
|
||||||
base_path: ./src
|
base_path: ./src
|
||||||
|
- project_name: MsSqlMigratorUtility
|
||||||
|
base_path: ./util
|
||||||
|
dotnet: true
|
||||||
- project_name: Notifications
|
- project_name: Notifications
|
||||||
base_path: ./src
|
base_path: ./src
|
||||||
|
- project_name: Scim
|
||||||
|
base_path: ./bitwarden_license/src
|
||||||
|
dotnet: true
|
||||||
- project_name: Server
|
- project_name: Server
|
||||||
base_path: ./util
|
base_path: ./util
|
||||||
- project_name: Setup
|
- project_name: Setup
|
||||||
@ -123,16 +133,13 @@ jobs:
|
|||||||
- project_name: Sso
|
- project_name: Sso
|
||||||
base_path: ./bitwarden_license/src
|
base_path: ./bitwarden_license/src
|
||||||
node: true
|
node: true
|
||||||
- project_name: Scim
|
|
||||||
base_path: ./bitwarden_license/src
|
|
||||||
dotnet: true
|
|
||||||
- project_name: MsSqlMigratorUtility
|
|
||||||
base_path: ./util
|
|
||||||
dotnet: true
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||||
|
|
||||||
|
- name: Set up dotnet
|
||||||
|
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
|
||||||
|
|
||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
|
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
|
||||||
with:
|
with:
|
||||||
@ -194,64 +201,48 @@ jobs:
|
|||||||
include:
|
include:
|
||||||
- project_name: Admin
|
- project_name: Admin
|
||||||
base_path: ./src
|
base_path: ./src
|
||||||
docker_repos: [bitwarden, bitwardenprod.azurecr.io, bitwardenqa.azurecr.io]
|
|
||||||
dotnet: true
|
dotnet: true
|
||||||
- project_name: Api
|
- project_name: Api
|
||||||
base_path: ./src
|
base_path: ./src
|
||||||
docker_repos: [bitwarden, bitwardenprod.azurecr.io, bitwardenqa.azurecr.io]
|
|
||||||
dotnet: true
|
dotnet: true
|
||||||
- project_name: Attachments
|
- project_name: Attachments
|
||||||
base_path: ./util
|
base_path: ./util
|
||||||
docker_repos: [bitwarden, bitwardenprod.azurecr.io, bitwardenqa.azurecr.io]
|
- project_name: Billing
|
||||||
|
base_path: ./src
|
||||||
|
dotnet: true
|
||||||
- project_name: Events
|
- project_name: Events
|
||||||
base_path: ./src
|
base_path: ./src
|
||||||
docker_repos: [bitwarden, bitwardenprod.azurecr.io, bitwardenqa.azurecr.io]
|
|
||||||
dotnet: true
|
dotnet: true
|
||||||
- project_name: EventsProcessor
|
- project_name: EventsProcessor
|
||||||
base_path: ./src
|
base_path: ./src
|
||||||
docker_repos: [bitwardenprod.azurecr.io, bitwardenqa.azurecr.io]
|
|
||||||
dotnet: true
|
dotnet: true
|
||||||
- project_name: Icons
|
- project_name: Icons
|
||||||
base_path: ./src
|
base_path: ./src
|
||||||
docker_repos: [bitwarden, bitwardenprod.azurecr.io, bitwardenqa.azurecr.io]
|
|
||||||
dotnet: true
|
dotnet: true
|
||||||
- project_name: Identity
|
- project_name: Identity
|
||||||
base_path: ./src
|
base_path: ./src
|
||||||
docker_repos: [bitwarden, bitwardenprod.azurecr.io, bitwardenqa.azurecr.io]
|
|
||||||
dotnet: true
|
dotnet: true
|
||||||
- project_name: MsSql
|
- project_name: MsSql
|
||||||
base_path: ./util
|
base_path: ./util
|
||||||
docker_repos: [bitwarden, bitwardenprod.azurecr.io, bitwardenqa.azurecr.io]
|
- project_name: MsSqlMigratorUtility
|
||||||
|
base_path: ./util
|
||||||
|
dotnet: true
|
||||||
- project_name: Nginx
|
- project_name: Nginx
|
||||||
base_path: ./util
|
base_path: ./util
|
||||||
docker_repos: [bitwarden, bitwardenprod.azurecr.io, bitwardenqa.azurecr.io]
|
|
||||||
- project_name: Notifications
|
- project_name: Notifications
|
||||||
base_path: ./src
|
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
|
dotnet: true
|
||||||
- project_name: Scim
|
- project_name: Scim
|
||||||
base_path: ./bitwarden_license/src
|
base_path: ./bitwarden_license/src
|
||||||
docker_repos: [bitwarden, bitwardenprod.azurecr.io, bitwardenqa.azurecr.io]
|
|
||||||
dotnet: true
|
dotnet: true
|
||||||
- project_name: Billing
|
- project_name: Server
|
||||||
base_path: ./src
|
|
||||||
docker_repos: [bitwardenprod.azurecr.io, bitwardenqa.azurecr.io]
|
|
||||||
dotnet: true
|
|
||||||
- project_name: MsSqlMigratorUtility
|
|
||||||
base_path: ./util
|
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
|
dotnet: true
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
@ -271,14 +262,6 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
########## ACRs ##########
|
########## 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
|
- name: Login to Azure - PROD Subscription
|
||||||
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
|
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
|
||||||
with:
|
with:
|
||||||
@ -294,36 +277,11 @@ jobs:
|
|||||||
|
|
||||||
- name: Retrieve github PAT secrets
|
- name: Retrieve github PAT secrets
|
||||||
id: retrieve-secret-pat
|
id: retrieve-secret-pat
|
||||||
uses: bitwarden/gh-actions/get-keyvault-secrets@74f4ac01c9abe0a7331c9a5de822a558fd4a4710
|
uses: bitwarden/gh-actions/get-keyvault-secrets@f096207b7a2f31723165aee6ad03e91716686e78
|
||||||
with:
|
with:
|
||||||
keyvault: "bitwarden-ci"
|
keyvault: "bitwarden-ci"
|
||||||
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
|
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 ##########
|
########## Generate image tag and build Docker image ##########
|
||||||
- name: Generate Docker image tag
|
- name: Generate Docker image tag
|
||||||
id: tag
|
id: tag
|
||||||
@ -342,12 +300,12 @@ jobs:
|
|||||||
echo "PROJECT_NAME: $PROJECT_NAME"
|
echo "PROJECT_NAME: $PROJECT_NAME"
|
||||||
echo "project_name=$PROJECT_NAME" >> $GITHUB_OUTPUT
|
echo "project_name=$PROJECT_NAME" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Generate tag list
|
- name: Generate image full name
|
||||||
id: tag-list
|
id: image-name
|
||||||
env:
|
env:
|
||||||
IMAGE_TAG: ${{ steps.tag.outputs.image_tag }}
|
IMAGE_TAG: ${{ steps.tag.outputs.image_tag }}
|
||||||
PROJECT_NAME: ${{ steps.setup.outputs.project_name }}
|
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
|
- name: Get build artifact
|
||||||
if: ${{ matrix.dotnet }}
|
if: ${{ matrix.dotnet }}
|
||||||
@ -369,37 +327,29 @@ jobs:
|
|||||||
file: ${{ matrix.base_path }}/${{ matrix.project_name }}/Dockerfile
|
file: ${{ matrix.base_path }}/${{ matrix.project_name }}/Dockerfile
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.tag-list.outputs.tags }}
|
tags: ${{ steps.image-name.outputs.name }}
|
||||||
secrets: |
|
secrets: |
|
||||||
"GH_PAT=${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }}"
|
"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:
|
upload:
|
||||||
name: Upload
|
name: Upload
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
needs: build-docker
|
needs: build-docker
|
||||||
steps:
|
steps:
|
||||||
- name: Set up dotnet
|
|
||||||
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
|
|
||||||
with:
|
|
||||||
dotnet-version: "6.0.x"
|
|
||||||
|
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
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
|
- name: Restore
|
||||||
run: dotnet tool restore
|
run: dotnet tool restore
|
||||||
|
|
||||||
@ -408,14 +358,18 @@ jobs:
|
|||||||
github.ref == 'refs/heads/rc' ||
|
github.ref == 'refs/heads/rc' ||
|
||||||
github.ref == 'refs/heads/hotfix-rc'
|
github.ref == 'refs/heads/hotfix-rc'
|
||||||
run: |
|
run: |
|
||||||
# Set proper image based on branch
|
# Set proper setup image based on branch
|
||||||
if [[ "${{ github.ref }}" == "rc" ]]; then
|
case "${{ github.ref }}" in
|
||||||
SETUP_IMAGE="bitwarden/setup:rc"
|
"refs/heads/master")
|
||||||
elif [[ "${{ github.ref }}" == "hotfix-rc" ]]; then
|
SETUP_IMAGE="$_AZ_REGISTRY/setup:dev"
|
||||||
SETUP_IMAGE="bitwarden/setup:hotfix-rc"
|
;;
|
||||||
else
|
"refs/heads/rc")
|
||||||
SETUP_IMAGE="bitwarden/setup:dev"
|
SETUP_IMAGE="$_AZ_REGISTRY/setup:rc"
|
||||||
fi
|
;;
|
||||||
|
"refs/heads/hotfix-rc")
|
||||||
|
SETUP_IMAGE="$_AZ_REGISTRY/setup:hotfix-rc"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
STUB_OUTPUT=$(pwd)/docker-stub
|
STUB_OUTPUT=$(pwd)/docker-stub
|
||||||
|
|
||||||
@ -508,8 +462,7 @@ jobs:
|
|||||||
build-mssqlmigratorutility:
|
build-mssqlmigratorutility:
|
||||||
name: Build MsSqlMigratorUtility
|
name: Build MsSqlMigratorUtility
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
needs:
|
needs: lint
|
||||||
- lint
|
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
shell: bash
|
shell: bash
|
||||||
@ -521,11 +474,13 @@ jobs:
|
|||||||
- osx-x64
|
- osx-x64
|
||||||
- linux-x64
|
- linux-x64
|
||||||
- win-x64
|
- win-x64
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||||
|
|
||||||
|
- name: Set up dotnet
|
||||||
|
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
|
||||||
|
|
||||||
- name: Print environment
|
- name: Print environment
|
||||||
run: |
|
run: |
|
||||||
whoami
|
whoami
|
||||||
@ -539,7 +494,9 @@ jobs:
|
|||||||
dotnet restore
|
dotnet restore
|
||||||
|
|
||||||
- name: Publish project
|
- 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
|
- name: Upload project artifact Windows
|
||||||
if: ${{ contains(matrix.target, 'win') == true }}
|
if: ${{ contains(matrix.target, 'win') == true }}
|
||||||
@ -620,7 +577,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Retrieve secrets
|
- name: Retrieve secrets
|
||||||
id: retrieve-secrets
|
id: retrieve-secrets
|
||||||
uses: bitwarden/gh-actions/get-keyvault-secrets@74f4ac01c9abe0a7331c9a5de822a558fd4a4710
|
uses: bitwarden/gh-actions/get-keyvault-secrets@f096207b7a2f31723165aee6ad03e91716686e78
|
||||||
if: failure()
|
if: failure()
|
||||||
with:
|
with:
|
||||||
keyvault: "bitwarden-ci"
|
keyvault: "bitwarden-ci"
|
||||||
|
@ -92,7 +92,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Retrieve secrets
|
- name: Retrieve secrets
|
||||||
id: retrieve-secrets
|
id: retrieve-secrets
|
||||||
uses: bitwarden/gh-actions/get-keyvault-secrets@74f4ac01c9abe0a7331c9a5de822a558fd4a4710
|
uses: bitwarden/gh-actions/get-keyvault-secrets@f096207b7a2f31723165aee6ad03e91716686e78
|
||||||
if: failure()
|
if: failure()
|
||||||
with:
|
with:
|
||||||
keyvault: "bitwarden-ci"
|
keyvault: "bitwarden-ci"
|
||||||
|
139
.github/workflows/release.yml
vendored
139
.github/workflows/release.yml
vendored
@ -15,6 +15,9 @@ on:
|
|||||||
- Redeploy
|
- Redeploy
|
||||||
- Dry Run
|
- Dry Run
|
||||||
|
|
||||||
|
env:
|
||||||
|
_AZ_REGISTRY: 'bitwardenprod.azurecr.io'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
setup:
|
setup:
|
||||||
name: Setup
|
name: Setup
|
||||||
@ -38,7 +41,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Check Release Version
|
- name: Check Release Version
|
||||||
id: version
|
id: version
|
||||||
uses: bitwarden/gh-actions/release-version-check@74f4ac01c9abe0a7331c9a5de822a558fd4a4710
|
uses: bitwarden/gh-actions/release-version-check@f096207b7a2f31723165aee6ad03e91716686e78
|
||||||
with:
|
with:
|
||||||
release-type: ${{ github.event.inputs.release_type }}
|
release-type: ${{ github.event.inputs.release_type }}
|
||||||
project-type: dotnet
|
project-type: dotnet
|
||||||
@ -53,18 +56,17 @@ jobs:
|
|||||||
deploy:
|
deploy:
|
||||||
name: Deploy
|
name: Deploy
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
needs:
|
needs: setup
|
||||||
- setup
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- name: Api
|
|
||||||
- name: Admin
|
- name: Admin
|
||||||
|
- name: Api
|
||||||
- name: Billing
|
- name: Billing
|
||||||
- name: Events
|
- name: Events
|
||||||
- name: Sso
|
|
||||||
- name: Identity
|
- name: Identity
|
||||||
|
- name: Sso
|
||||||
steps:
|
steps:
|
||||||
- name: Setup
|
- name: Setup
|
||||||
id: setup
|
id: setup
|
||||||
@ -87,16 +89,16 @@ jobs:
|
|||||||
|
|
||||||
- name: Download latest Release ${{ matrix.name }} asset
|
- name: Download latest Release ${{ matrix.name }} asset
|
||||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||||
uses: bitwarden/gh-actions/download-artifacts@74f4ac01c9abe0a7331c9a5de822a558fd4a4710
|
uses: bitwarden/gh-actions/download-artifacts@f096207b7a2f31723165aee6ad03e91716686e78
|
||||||
with:
|
with:
|
||||||
workflow: build.yml
|
workflow: build.yml
|
||||||
workflow_conclusion: success
|
workflow_conclusion: success
|
||||||
branch: ${{ needs.setup.outputs.branch-name }}
|
branch: ${{ needs.setup.outputs.branch-name }}
|
||||||
artifacts: ${{ matrix.name }}.zip
|
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' }}
|
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
|
||||||
uses: bitwarden/gh-actions/download-artifacts@74f4ac01c9abe0a7331c9a5de822a558fd4a4710
|
uses: bitwarden/gh-actions/download-artifacts@f096207b7a2f31723165aee6ad03e91716686e78
|
||||||
with:
|
with:
|
||||||
workflow: build.yml
|
workflow: build.yml
|
||||||
workflow_conclusion: success
|
workflow_conclusion: success
|
||||||
@ -173,8 +175,7 @@ jobs:
|
|||||||
release-docker:
|
release-docker:
|
||||||
name: Build Docker images
|
name: Build Docker images
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
needs:
|
needs: setup
|
||||||
- setup
|
|
||||||
env:
|
env:
|
||||||
_RELEASE_VERSION: ${{ needs.setup.outputs.release_version }}
|
_RELEASE_VERSION: ${{ needs.setup.outputs.release_version }}
|
||||||
_BRANCH_NAME: ${{ needs.setup.outputs.branch-name }}
|
_BRANCH_NAME: ${{ needs.setup.outputs.branch-name }}
|
||||||
@ -183,40 +184,21 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- project_name: Admin
|
- project_name: Admin
|
||||||
origin_docker_repo: bitwarden
|
|
||||||
- project_name: Api
|
- project_name: Api
|
||||||
origin_docker_repo: bitwarden
|
|
||||||
- project_name: Attachments
|
- 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
|
- 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
|
- 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:
|
steps:
|
||||||
- name: Print environment
|
- name: Print environment
|
||||||
env:
|
env:
|
||||||
@ -239,51 +221,6 @@ jobs:
|
|||||||
echo "PROJECT_NAME: $PROJECT_NAME"
|
echo "PROJECT_NAME: $PROJECT_NAME"
|
||||||
echo "project_name=$PROJECT_NAME" >> $GITHUB_OUTPUT
|
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 ##########
|
########## ACR PROD ##########
|
||||||
- name: Login to Azure - PROD Subscription
|
- name: Login to Azure - PROD Subscription
|
||||||
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
|
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
|
||||||
@ -291,41 +228,39 @@ jobs:
|
|||||||
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
||||||
|
|
||||||
- name: Login to Azure ACR
|
- 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
|
- name: Pull latest project image
|
||||||
if: matrix.origin_docker_repo == 'bitwardenprod.azurecr.io'
|
|
||||||
env:
|
env:
|
||||||
PROJECT_NAME: ${{ steps.setup.outputs.project_name }}
|
PROJECT_NAME: ${{ steps.setup.outputs.project_name }}
|
||||||
ORIGIN_REGISTRY: ${{ matrix.origin_docker_repo }}
|
|
||||||
run: |
|
run: |
|
||||||
if [[ "${{ github.event.inputs.release_type }}" == "Dry Run" ]]; then
|
if [[ "${{ github.event.inputs.release_type }}" == "Dry Run" ]]; then
|
||||||
docker pull $ORIGIN_REGISTRY/$PROJECT_NAME:dev
|
docker pull $_AZ_REGISTRY/$PROJECT_NAME:latest
|
||||||
else
|
else
|
||||||
docker pull $ORIGIN_REGISTRY/$PROJECT_NAME:$_BRANCH_NAME
|
docker pull $_AZ_REGISTRY/$PROJECT_NAME:$_BRANCH_NAME
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Tag version and latest
|
- name: Tag version and latest
|
||||||
env:
|
env:
|
||||||
PROJECT_NAME: ${{ steps.setup.outputs.project_name }}
|
PROJECT_NAME: ${{ steps.setup.outputs.project_name }}
|
||||||
REGISTRY: bitwardenprod.azurecr.io
|
|
||||||
ORIGIN_REGISTRY: ${{ matrix.origin_docker_repo }}
|
|
||||||
run: |
|
run: |
|
||||||
if [[ "${{ github.event.inputs.release_type }}" == "Dry Run" ]]; then
|
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
|
else
|
||||||
docker tag $ORIGIN_REGISTRY/$PROJECT_NAME:$_BRANCH_NAME $REGISTRY/$PROJECT_NAME:$_RELEASE_VERSION
|
docker tag $_AZ_REGISTRY/$PROJECT_NAME:$_BRANCH_NAME $_AZ_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:latest
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Push version and latest image
|
- name: Push version and latest image
|
||||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
|
||||||
env:
|
env:
|
||||||
PROJECT_NAME: ${{ steps.setup.outputs.project_name }}
|
PROJECT_NAME: ${{ steps.setup.outputs.project_name }}
|
||||||
REGISTRY: bitwardenprod.azurecr.io
|
|
||||||
run: |
|
run: |
|
||||||
docker push $REGISTRY/$PROJECT_NAME:$_RELEASE_VERSION
|
if [[ "${{ github.event.inputs.release_type }}" == "Dry Run" ]]; then
|
||||||
docker push $REGISTRY/$PROJECT_NAME:latest
|
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
|
- name: Log out of Docker
|
||||||
run: docker logout
|
run: docker logout
|
||||||
@ -339,7 +274,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Download latest Release Docker Stubs
|
- name: Download latest Release Docker Stubs
|
||||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||||
uses: bitwarden/gh-actions/download-artifacts@74f4ac01c9abe0a7331c9a5de822a558fd4a4710
|
uses: bitwarden/gh-actions/download-artifacts@f096207b7a2f31723165aee6ad03e91716686e78
|
||||||
with:
|
with:
|
||||||
workflow: build.yml
|
workflow: build.yml
|
||||||
workflow_conclusion: success
|
workflow_conclusion: success
|
||||||
@ -350,9 +285,9 @@ jobs:
|
|||||||
docker-stub-EU-sha256.txt,
|
docker-stub-EU-sha256.txt,
|
||||||
swagger.json"
|
swagger.json"
|
||||||
|
|
||||||
- name: Download latest Release Docker Stubs
|
- name: Dry Run - Download latest Release Docker Stubs
|
||||||
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
|
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
|
||||||
uses: bitwarden/gh-actions/download-artifacts@74f4ac01c9abe0a7331c9a5de822a558fd4a4710
|
uses: bitwarden/gh-actions/download-artifacts@f096207b7a2f31723165aee6ad03e91716686e78
|
||||||
with:
|
with:
|
||||||
workflow: build.yml
|
workflow: build.yml
|
||||||
workflow_conclusion: success
|
workflow_conclusion: success
|
||||||
|
4
.github/workflows/version-bump.yml
vendored
4
.github/workflows/version-bump.yml
vendored
@ -23,7 +23,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Retrieve secrets
|
- name: Retrieve secrets
|
||||||
id: retrieve-secrets
|
id: retrieve-secrets
|
||||||
uses: bitwarden/gh-actions/get-keyvault-secrets@74f4ac01c9abe0a7331c9a5de822a558fd4a4710
|
uses: bitwarden/gh-actions/get-keyvault-secrets@f096207b7a2f31723165aee6ad03e91716686e78
|
||||||
with:
|
with:
|
||||||
keyvault: "bitwarden-ci"
|
keyvault: "bitwarden-ci"
|
||||||
secrets: "github-gpg-private-key, github-gpg-private-key-passphrase"
|
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 }}
|
run: git switch -c version_bump_${{ github.event.inputs.version_number }}
|
||||||
|
|
||||||
- name: Bump Version - Props
|
- name: Bump Version - Props
|
||||||
uses: bitwarden/gh-actions/version-bump@74f4ac01c9abe0a7331c9a5de822a558fd4a4710
|
uses: bitwarden/gh-actions/version-bump@f096207b7a2f31723165aee6ad03e91716686e78
|
||||||
with:
|
with:
|
||||||
version: ${{ github.event.inputs.version_number }}
|
version: ${{ github.event.inputs.version_number }}
|
||||||
file_path: "Directory.Build.props"
|
file_path: "Directory.Build.props"
|
||||||
|
2
.github/workflows/workflow-linter.yml
vendored
2
.github/workflows/workflow-linter.yml
vendored
@ -8,4 +8,4 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
call-workflow:
|
call-workflow:
|
||||||
uses: bitwarden/gh-actions/.github/workflows/workflow-linter.yml@74f4ac01c9abe0a7331c9a5de822a558fd4a4710
|
uses: bitwarden/gh-actions/.github/workflows/workflow-linter.yml@f096207b7a2f31723165aee6ad03e91716686e78
|
||||||
|
14
.vscode/launch.json
vendored
14
.vscode/launch.json
vendored
@ -101,7 +101,6 @@
|
|||||||
"stopAtEntry": false,
|
"stopAtEntry": false,
|
||||||
"env": {
|
"env": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||||
"WEBSITE_INSTANCE_ID": "dev",
|
|
||||||
},
|
},
|
||||||
"sourceFileMap": {
|
"sourceFileMap": {
|
||||||
"/Views": "${workspaceFolder}/Views"
|
"/Views": "${workspaceFolder}/Views"
|
||||||
@ -124,7 +123,6 @@
|
|||||||
"stopAtEntry": false,
|
"stopAtEntry": false,
|
||||||
"env": {
|
"env": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||||
"WEBSITE_INSTANCE_ID": "dev",
|
|
||||||
},
|
},
|
||||||
"sourceFileMap": {
|
"sourceFileMap": {
|
||||||
"/Views": "${workspaceFolder}/Views"
|
"/Views": "${workspaceFolder}/Views"
|
||||||
@ -147,7 +145,6 @@
|
|||||||
"stopAtEntry": false,
|
"stopAtEntry": false,
|
||||||
"env": {
|
"env": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||||
"WEBSITE_INSTANCE_ID": "dev",
|
|
||||||
},
|
},
|
||||||
"sourceFileMap": {
|
"sourceFileMap": {
|
||||||
"/Views": "${workspaceFolder}/Views"
|
"/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",
|
"OS-COMMENT5": "Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser",
|
||||||
"env": {
|
"env": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||||
"WEBSITE_INSTANCE_ID": "dev",
|
|
||||||
},
|
},
|
||||||
"sourceFileMap": {
|
"sourceFileMap": {
|
||||||
"/Views": "${workspaceFolder}/Views"
|
"/Views": "${workspaceFolder}/Views"
|
||||||
@ -195,7 +191,6 @@
|
|||||||
"stopAtEntry": false,
|
"stopAtEntry": false,
|
||||||
"env": {
|
"env": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||||
"WEBSITE_INSTANCE_ID": "dev",
|
|
||||||
},
|
},
|
||||||
"sourceFileMap": {
|
"sourceFileMap": {
|
||||||
"/Views": "${workspaceFolder}/Views"
|
"/Views": "${workspaceFolder}/Views"
|
||||||
@ -218,7 +213,6 @@
|
|||||||
"stopAtEntry": false,
|
"stopAtEntry": false,
|
||||||
"env": {
|
"env": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||||
"WEBSITE_INSTANCE_ID": "dev",
|
|
||||||
},
|
},
|
||||||
"sourceFileMap": {
|
"sourceFileMap": {
|
||||||
"/Views": "${workspaceFolder}/Views"
|
"/Views": "${workspaceFolder}/Views"
|
||||||
@ -241,7 +235,6 @@
|
|||||||
"stopAtEntry": false,
|
"stopAtEntry": false,
|
||||||
"env": {
|
"env": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||||
"WEBSITE_INSTANCE_ID": "dev",
|
|
||||||
},
|
},
|
||||||
"sourceFileMap": {
|
"sourceFileMap": {
|
||||||
"/Views": "${workspaceFolder}/Views"
|
"/Views": "${workspaceFolder}/Views"
|
||||||
@ -264,7 +257,6 @@
|
|||||||
"stopAtEntry": false,
|
"stopAtEntry": false,
|
||||||
"env": {
|
"env": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||||
"WEBSITE_INSTANCE_ID": "dev",
|
|
||||||
},
|
},
|
||||||
"sourceFileMap": {
|
"sourceFileMap": {
|
||||||
"/Views": "${workspaceFolder}/Views"
|
"/Views": "${workspaceFolder}/Views"
|
||||||
@ -289,7 +281,6 @@
|
|||||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||||
"ASPNETCORE_URLS": "http://localhost:33657",
|
"ASPNETCORE_URLS": "http://localhost:33657",
|
||||||
"developSelfHosted": "true",
|
"developSelfHosted": "true",
|
||||||
"WEBSITE_INSTANCE_ID": "dev",
|
|
||||||
},
|
},
|
||||||
"sourceFileMap": {
|
"sourceFileMap": {
|
||||||
"/Views": "${workspaceFolder}/Views"
|
"/Views": "${workspaceFolder}/Views"
|
||||||
@ -314,7 +305,6 @@
|
|||||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||||
"ASPNETCORE_URLS": "http://localhost:4001",
|
"ASPNETCORE_URLS": "http://localhost:4001",
|
||||||
"developSelfHosted": "true",
|
"developSelfHosted": "true",
|
||||||
"WEBSITE_INSTANCE_ID": "dev",
|
|
||||||
},
|
},
|
||||||
"sourceFileMap": {
|
"sourceFileMap": {
|
||||||
"/Views": "${workspaceFolder}/Views"
|
"/Views": "${workspaceFolder}/Views"
|
||||||
@ -341,7 +331,6 @@
|
|||||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||||
"ASPNETCORE_URLS": "http://localhost:62912",
|
"ASPNETCORE_URLS": "http://localhost:62912",
|
||||||
"developSelfHosted": "true",
|
"developSelfHosted": "true",
|
||||||
"WEBSITE_INSTANCE_ID": "dev",
|
|
||||||
},
|
},
|
||||||
"sourceFileMap": {
|
"sourceFileMap": {
|
||||||
"/Views": "${workspaceFolder}/Views"
|
"/Views": "${workspaceFolder}/Views"
|
||||||
@ -366,7 +355,6 @@
|
|||||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||||
"ASPNETCORE_URLS": "http://localhost:51822",
|
"ASPNETCORE_URLS": "http://localhost:51822",
|
||||||
"developSelfHosted": "true",
|
"developSelfHosted": "true",
|
||||||
"WEBSITE_INSTANCE_ID": "dev",
|
|
||||||
},
|
},
|
||||||
"sourceFileMap": {
|
"sourceFileMap": {
|
||||||
"/Views": "${workspaceFolder}/Views"
|
"/Views": "${workspaceFolder}/Views"
|
||||||
@ -391,7 +379,6 @@
|
|||||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||||
"ASPNETCORE_URLS": "http://localhost:61841",
|
"ASPNETCORE_URLS": "http://localhost:61841",
|
||||||
"developSelfHosted": "true",
|
"developSelfHosted": "true",
|
||||||
"WEBSITE_INSTANCE_ID": "dev",
|
|
||||||
},
|
},
|
||||||
"sourceFileMap": {
|
"sourceFileMap": {
|
||||||
"/Views": "${workspaceFolder}/Views"
|
"/Views": "${workspaceFolder}/Views"
|
||||||
@ -416,7 +403,6 @@
|
|||||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||||
"ASPNETCORE_URLS": "http://localhost:46274",
|
"ASPNETCORE_URLS": "http://localhost:46274",
|
||||||
"developSelfHosted": "true",
|
"developSelfHosted": "true",
|
||||||
"WEBSITE_INSTANCE_ID": "dev",
|
|
||||||
},
|
},
|
||||||
"sourceFileMap": {
|
"sourceFileMap": {
|
||||||
"/Views": "${workspaceFolder}/Views"
|
"/Views": "${workspaceFolder}/Views"
|
||||||
|
@ -1,8 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<configuration>
|
<configuration>
|
||||||
<packageSources>
|
<!-- No configured package sources besides the default. -->
|
||||||
<!--To inherit the global NuGet package sources remove the <clear/> line below -->
|
|
||||||
<clear />
|
|
||||||
<add key="api.nuget.org" value="https://api.nuget.org/v3/index.json" />
|
|
||||||
</packageSources>
|
|
||||||
</configuration>
|
</configuration>
|
||||||
|
@ -940,15 +940,6 @@
|
|||||||
"System.Security.Cryptography.Pkcs": "6.0.0"
|
"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": {
|
"NETStandard.Library": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "1.6.1",
|
"resolved": "1.6.1",
|
||||||
@ -2675,75 +2666,74 @@
|
|||||||
"commercial.core": {
|
"commercial.core": {
|
||||||
"type": "Project",
|
"type": "Project",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Core": "2023.7.2"
|
"Core": "[2023.7.2, )"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"type": "Project",
|
"type": "Project",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"AutoFixture.AutoNSubstitute": "4.17.0",
|
"AutoFixture.AutoNSubstitute": "[4.17.0, )",
|
||||||
"AutoFixture.Xunit2": "4.17.0",
|
"AutoFixture.Xunit2": "[4.17.0, )",
|
||||||
"Core": "2023.7.2",
|
"Core": "[2023.7.2, )",
|
||||||
"Kralizek.AutoFixture.Extensions.MockHttp": "1.2.0",
|
"Kralizek.AutoFixture.Extensions.MockHttp": "[1.2.0, )",
|
||||||
"Microsoft.NET.Test.Sdk": "17.1.0",
|
"Microsoft.NET.Test.Sdk": "[17.1.0, )",
|
||||||
"NSubstitute": "4.3.0",
|
"NSubstitute": "[4.3.0, )",
|
||||||
"xunit": "2.4.1"
|
"xunit": "[2.4.1, )"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"core": {
|
"core": {
|
||||||
"type": "Project",
|
"type": "Project",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"AWSSDK.SQS": "3.7.2.47",
|
"AWSSDK.SQS": "[3.7.2.47, )",
|
||||||
"AWSSDK.SimpleEmail": "3.7.0.150",
|
"AWSSDK.SimpleEmail": "[3.7.0.150, )",
|
||||||
"AspNetCoreRateLimit": "4.0.2",
|
"AspNetCoreRateLimit": "[4.0.2, )",
|
||||||
"AspNetCoreRateLimit.Redis": "1.0.1",
|
"AspNetCoreRateLimit.Redis": "[1.0.1, )",
|
||||||
"Azure.Extensions.AspNetCore.DataProtection.Blobs": "1.3.2",
|
"Azure.Extensions.AspNetCore.DataProtection.Blobs": "[1.3.2, )",
|
||||||
"Azure.Messaging.ServiceBus": "7.15.0",
|
"Azure.Messaging.ServiceBus": "[7.15.0, )",
|
||||||
"Azure.Storage.Blobs": "12.14.1",
|
"Azure.Storage.Blobs": "[12.14.1, )",
|
||||||
"Azure.Storage.Queues": "12.12.0",
|
"Azure.Storage.Queues": "[12.12.0, )",
|
||||||
"BitPay.Light": "1.0.1907",
|
"BitPay.Light": "[1.0.1907, )",
|
||||||
"Braintree": "5.12.0",
|
"Braintree": "[5.12.0, )",
|
||||||
"DnsClient": "1.7.0",
|
"DnsClient": "[1.7.0, )",
|
||||||
"Fido2.AspNet": "3.0.1",
|
"Fido2.AspNet": "[3.0.1, )",
|
||||||
"Handlebars.Net": "2.1.2",
|
"Handlebars.Net": "[2.1.2, )",
|
||||||
"IdentityServer4": "4.1.2",
|
"IdentityServer4": "[4.1.2, )",
|
||||||
"IdentityServer4.AccessTokenValidation": "3.0.1",
|
"IdentityServer4.AccessTokenValidation": "[3.0.1, )",
|
||||||
"LaunchDarkly.ServerSdk": "7.0.0",
|
"LaunchDarkly.ServerSdk": "[7.0.0, )",
|
||||||
"MailKit": "3.2.0",
|
"MailKit": "[3.2.0, )",
|
||||||
"Microsoft.AspNetCore.Authentication.JwtBearer": "6.0.4",
|
"Microsoft.AspNetCore.Authentication.JwtBearer": "[6.0.4, )",
|
||||||
"Microsoft.Azure.Cosmos.Table": "1.0.8",
|
"Microsoft.Azure.Cosmos.Table": "[1.0.8, )",
|
||||||
"Microsoft.Azure.NotificationHubs": "4.1.0",
|
"Microsoft.Azure.NotificationHubs": "[4.1.0, )",
|
||||||
"Microsoft.Data.SqlClient": "5.0.1",
|
"Microsoft.Data.SqlClient": "[5.0.1, )",
|
||||||
"Microsoft.Extensions.Caching.StackExchangeRedis": "6.0.6",
|
"Microsoft.Extensions.Caching.StackExchangeRedis": "[6.0.6, )",
|
||||||
"Microsoft.Extensions.Configuration.EnvironmentVariables": "6.0.1",
|
"Microsoft.Extensions.Configuration.EnvironmentVariables": "[6.0.1, )",
|
||||||
"Microsoft.Extensions.Configuration.UserSecrets": "6.0.1",
|
"Microsoft.Extensions.Configuration.UserSecrets": "[6.0.1, )",
|
||||||
"Microsoft.Extensions.Identity.Stores": "6.0.4",
|
"Microsoft.Extensions.Identity.Stores": "[6.0.4, )",
|
||||||
"Newtonsoft.Json": "13.0.1",
|
"Newtonsoft.Json": "[13.0.1, )",
|
||||||
"Otp.NET": "1.2.2",
|
"Otp.NET": "[1.2.2, )",
|
||||||
"Quartz": "3.4.0",
|
"Quartz": "[3.4.0, )",
|
||||||
"SendGrid": "9.27.0",
|
"SendGrid": "[9.27.0, )",
|
||||||
"Sentry.Serilog": "3.16.0",
|
"Sentry.Serilog": "[3.16.0, )",
|
||||||
"Serilog.AspNetCore": "5.0.0",
|
"Serilog.AspNetCore": "[5.0.0, )",
|
||||||
"Serilog.Extensions.Logging": "3.1.0",
|
"Serilog.Extensions.Logging": "[3.1.0, )",
|
||||||
"Serilog.Extensions.Logging.File": "2.0.0",
|
"Serilog.Extensions.Logging.File": "[2.0.0, )",
|
||||||
"Serilog.Sinks.AzureCosmosDB": "2.0.0",
|
"Serilog.Sinks.AzureCosmosDB": "[2.0.0, )",
|
||||||
"Serilog.Sinks.SyslogMessages": "2.0.6",
|
"Serilog.Sinks.SyslogMessages": "[2.0.6, )",
|
||||||
"Stripe.net": "40.0.0",
|
"Stripe.net": "[40.0.0, )",
|
||||||
"YubicoDotNetClient": "1.2.0"
|
"YubicoDotNetClient": "[1.2.0, )"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"core.test": {
|
"core.test": {
|
||||||
"type": "Project",
|
"type": "Project",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"AutoFixture.AutoNSubstitute": "4.17.0",
|
"AutoFixture.AutoNSubstitute": "[4.17.0, )",
|
||||||
"AutoFixture.Xunit2": "4.17.0",
|
"AutoFixture.Xunit2": "[4.17.0, )",
|
||||||
"Common": "2023.7.2",
|
"Common": "[2023.7.2, )",
|
||||||
"Core": "2023.7.2",
|
"Core": "[2023.7.2, )",
|
||||||
"Kralizek.AutoFixture.Extensions.MockHttp": "1.2.0",
|
"Kralizek.AutoFixture.Extensions.MockHttp": "[1.2.0, )",
|
||||||
"Microsoft.NET.Test.Sdk": "17.1.0",
|
"Microsoft.NET.Test.Sdk": "[17.1.0, )",
|
||||||
"Moq": "4.17.2",
|
"NSubstitute": "[4.3.0, )",
|
||||||
"NSubstitute": "4.3.0",
|
"xunit": "[2.4.1, )"
|
||||||
"xunit": "2.4.1"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"sdk": {
|
"sdk": {
|
||||||
"version": "6.0.100",
|
"version": "6.0.413",
|
||||||
"rollForward": "latestFeature"
|
"rollForward": "latestFeature"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
document.getElementById('@(nameof(Model.Plan))').value = selectText;
|
document.getElementById('@(nameof(Model.Plan))').value = selectText;
|
||||||
togglePlanSettings(selectEl.options[selectEl.selectedIndex].value);
|
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 gateway = document.getElementById('@(nameof(Model.Gateway))');
|
||||||
const customerId = document.getElementById('@(nameof(Model.GatewayCustomerId))');
|
const customerId = document.getElementById('@(nameof(Model.GatewayCustomerId))');
|
||||||
if (!gateway || gateway.value === '' || !customerId || customerId.value === '') {
|
if (!gateway || gateway.value === '' || !customerId || customerId.value === '') {
|
||||||
@ -19,7 +19,7 @@
|
|||||||
+ customerId.value, '_blank');
|
+ 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 gateway = document.getElementById('@(nameof(Model.Gateway))');
|
||||||
const subId = document.getElementById('@(nameof(Model.GatewaySubscriptionId))');
|
const subId = document.getElementById('@(nameof(Model.GatewaySubscriptionId))');
|
||||||
if (!gateway || gateway.value === '' || !subId || subId.value === '') {
|
if (!gateway || gateway.value === '' || !subId || subId.value === '') {
|
||||||
@ -34,24 +34,24 @@
|
|||||||
});
|
});
|
||||||
document.getElementById('@(nameof(Model.UseSecretsManager))').addEventListener('change', (event) => {
|
document.getElementById('@(nameof(Model.UseSecretsManager))').addEventListener('change', (event) => {
|
||||||
document.getElementById('organization-secrets-configuration').hidden = !event.target.checked;
|
document.getElementById('organization-secrets-configuration').hidden = !event.target.checked;
|
||||||
|
|
||||||
if (event.target.checked) {
|
if (event.target.checked) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('@(nameof(Model.SmSeats))').value = '';
|
document.getElementById('@(nameof(Model.SmSeats))').value = '';
|
||||||
document.getElementById('@(nameof(Model.MaxAutoscaleSmSeats))').value = '';
|
document.getElementById('@(nameof(Model.MaxAutoscaleSmSeats))').value = '';
|
||||||
document.getElementById('@(nameof(Model.SmServiceAccounts))').value = '';
|
document.getElementById('@(nameof(Model.SmServiceAccounts))').value = '';
|
||||||
document.getElementById('@(nameof(Model.MaxAutoscaleSmServiceAccounts))').value = '';
|
document.getElementById('@(nameof(Model.MaxAutoscaleSmServiceAccounts))').value = '';
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|
||||||
function togglePlanSettings(planType) {
|
function togglePlanSettings(planType) {
|
||||||
document.getElementById('@(nameof(Model.PlanType))').value = planType;
|
document.getElementById('@(nameof(Model.PlanType))').value = planType;
|
||||||
switch(planType) {
|
switch(planType) {
|
||||||
case '@((byte)Bit.Core.Enums.PlanType.TeamsMonthly)':
|
case '@((byte)Bit.Core.Enums.PlanType.TeamsMonthly)':
|
||||||
case '@((byte)Bit.Core.Enums.PlanType.TeamsAnnually)':
|
case '@((byte)Bit.Core.Enums.PlanType.TeamsAnnually)':
|
||||||
// Plan
|
// Plan
|
||||||
document.getElementById('@(nameof(Model.Seats))').value = '10';
|
document.getElementById('@(nameof(Model.Seats))').value = '10';
|
||||||
document.getElementById('@(nameof(Model.MaxCollections))').value = '';
|
document.getElementById('@(nameof(Model.MaxCollections))').value = '';
|
||||||
document.getElementById('@(nameof(Model.MaxStorageGb))').value = '1';
|
document.getElementById('@(nameof(Model.MaxStorageGb))').value = '1';
|
||||||
@ -66,7 +66,7 @@
|
|||||||
document.getElementById('@(nameof(Model.UseDirectory))').checked = true;
|
document.getElementById('@(nameof(Model.UseDirectory))').checked = true;
|
||||||
document.getElementById('@(nameof(Model.UseEvents))').checked = true;
|
document.getElementById('@(nameof(Model.UseEvents))').checked = true;
|
||||||
document.getElementById('@(nameof(Model.UsersGetPremium))').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.UseTotp))').checked = true;
|
||||||
document.getElementById('@(nameof(Model.Use2fa))').checked = true;
|
document.getElementById('@(nameof(Model.Use2fa))').checked = true;
|
||||||
document.getElementById('@(nameof(Model.UseApi))').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.ExpirationDate))').value = '@Model.FourteenDayExpirationDate';
|
||||||
document.getElementById('@(nameof(Model.SalesAssistedTrialStarted))').value = true;
|
document.getElementById('@(nameof(Model.SalesAssistedTrialStarted))').value = true;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case '@((byte)Bit.Core.Enums.PlanType.EnterpriseMonthly)':
|
case '@((byte)Bit.Core.Enums.PlanType.EnterpriseMonthly)':
|
||||||
case '@((byte)Bit.Core.Enums.PlanType.EnterpriseAnnually)':
|
case '@((byte)Bit.Core.Enums.PlanType.EnterpriseAnnually)':
|
||||||
// Plan
|
// Plan
|
||||||
@ -109,6 +109,6 @@
|
|||||||
document.getElementById('@(nameof(Model.SalesAssistedTrialStarted))').value = true;
|
document.getElementById('@(nameof(Model.SalesAssistedTrialStarted))').value = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -112,7 +112,7 @@ public class CollectionsController : Controller
|
|||||||
[HttpGet("")]
|
[HttpGet("")]
|
||||||
public async Task<ListResponseModel<CollectionResponseModel>> Get(Guid orgId)
|
public async Task<ListResponseModel<CollectionResponseModel>> Get(Guid orgId)
|
||||||
{
|
{
|
||||||
IEnumerable<Collection> orgCollections = await _collectionService.GetOrganizationCollections(orgId);
|
IEnumerable<Collection> orgCollections = await _collectionService.GetOrganizationCollectionsAsync(orgId);
|
||||||
|
|
||||||
var responses = orgCollections.Select(c => new CollectionResponseModel(c));
|
var responses = orgCollections.Select(c => new CollectionResponseModel(c));
|
||||||
return new ListResponseModel<CollectionResponseModel>(responses);
|
return new ListResponseModel<CollectionResponseModel>(responses);
|
||||||
@ -209,7 +209,7 @@ public class CollectionsController : Controller
|
|||||||
throw new NotFoundException();
|
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);
|
var filteredCollections = userCollections.Where(c => collectionIds.Contains(c.Id) && c.OrganizationId == orgId);
|
||||||
|
|
||||||
if (!filteredCollections.Any())
|
if (!filteredCollections.Any())
|
||||||
|
@ -40,7 +40,7 @@ public class OrganizationExportController : Controller
|
|||||||
{
|
{
|
||||||
var userId = _userService.GetProperUserId(User).Value;
|
var userId = _userService.GetProperUserId(User).Value;
|
||||||
|
|
||||||
IEnumerable<Collection> orgCollections = await _collectionService.GetOrganizationCollections(organizationId);
|
IEnumerable<Collection> orgCollections = await _collectionService.GetOrganizationCollectionsAsync(organizationId);
|
||||||
(IEnumerable<CipherOrganizationDetails> orgCiphers, Dictionary<Guid, IGrouping<Guid, CollectionCipher>> collectionCiphersGroupDict) = await _cipherService.GetOrganizationCiphers(userId, organizationId);
|
(IEnumerable<CipherOrganizationDetails> orgCiphers, Dictionary<Guid, IGrouping<Guid, CollectionCipher>> collectionCiphersGroupDict) = await _cipherService.GetOrganizationCiphers(userId, organizationId);
|
||||||
|
|
||||||
if (_currentContext.ClientVersion == null || _currentContext.ClientVersion >= new Version("2023.1.0"))
|
if (_currentContext.ClientVersion == null || _currentContext.ClientVersion >= new Version("2023.1.0"))
|
||||||
|
@ -8,6 +8,7 @@ public class ProjectCreateRequestModel
|
|||||||
{
|
{
|
||||||
[Required]
|
[Required]
|
||||||
[EncryptedString]
|
[EncryptedString]
|
||||||
|
[EncryptedStringLength(1000)]
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
|
|
||||||
public Project ToProject(Guid organizationId)
|
public Project ToProject(Guid organizationId)
|
||||||
|
@ -8,6 +8,7 @@ public class ProjectUpdateRequestModel
|
|||||||
{
|
{
|
||||||
[Required]
|
[Required]
|
||||||
[EncryptedString]
|
[EncryptedString]
|
||||||
|
[EncryptedStringLength(1000)]
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
|
|
||||||
public Project ToProject(Guid id)
|
public Project ToProject(Guid id)
|
||||||
|
@ -8,14 +8,17 @@ public class SecretCreateRequestModel : IValidatableObject
|
|||||||
{
|
{
|
||||||
[Required]
|
[Required]
|
||||||
[EncryptedString]
|
[EncryptedString]
|
||||||
|
[EncryptedStringLength(1000)]
|
||||||
public string Key { get; set; }
|
public string Key { get; set; }
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
[EncryptedString]
|
[EncryptedString]
|
||||||
|
[EncryptedStringLength(5000)]
|
||||||
public string Value { get; set; }
|
public string Value { get; set; }
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
[EncryptedString]
|
[EncryptedString]
|
||||||
|
[EncryptedStringLength(10000)]
|
||||||
public string Note { get; set; }
|
public string Note { get; set; }
|
||||||
|
|
||||||
public Guid[] ProjectIds { get; set; }
|
public Guid[] ProjectIds { get; set; }
|
||||||
|
@ -8,14 +8,17 @@ public class SecretUpdateRequestModel : IValidatableObject
|
|||||||
{
|
{
|
||||||
[Required]
|
[Required]
|
||||||
[EncryptedString]
|
[EncryptedString]
|
||||||
|
[EncryptedStringLength(1000)]
|
||||||
public string Key { get; set; }
|
public string Key { get; set; }
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
[EncryptedString]
|
[EncryptedString]
|
||||||
|
[EncryptedStringLength(5000)]
|
||||||
public string Value { get; set; }
|
public string Value { get; set; }
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
[EncryptedString]
|
[EncryptedString]
|
||||||
|
[EncryptedStringLength(10000)]
|
||||||
public string Note { get; set; }
|
public string Note { get; set; }
|
||||||
|
|
||||||
public Guid[] ProjectIds { get; set; }
|
public Guid[] ProjectIds { get; set; }
|
||||||
|
@ -8,6 +8,7 @@ public class ServiceAccountUpdateRequestModel
|
|||||||
{
|
{
|
||||||
[Required]
|
[Required]
|
||||||
[EncryptedString]
|
[EncryptedString]
|
||||||
|
[EncryptedStringLength(1000)]
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
|
|
||||||
public ServiceAccount ToServiceAccount(Guid id)
|
public ServiceAccount ToServiceAccount(Guid id)
|
||||||
|
@ -8,6 +8,7 @@ public class ServiceAccountCreateRequestModel
|
|||||||
{
|
{
|
||||||
[Required]
|
[Required]
|
||||||
[EncryptedString]
|
[EncryptedString]
|
||||||
|
[EncryptedStringLength(1000)]
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
|
|
||||||
public ServiceAccount ToServiceAccount(Guid organizationId)
|
public ServiceAccount ToServiceAccount(Guid organizationId)
|
||||||
|
@ -341,7 +341,7 @@ public class CurrentContext : ICurrentContext
|
|||||||
|
|
||||||
public async Task<bool> ViewAllCollections(Guid orgId)
|
public async Task<bool> ViewAllCollections(Guid orgId)
|
||||||
{
|
{
|
||||||
return await CreateNewCollections(orgId) || await EditAnyCollection(orgId) || await DeleteAnyCollection(orgId);
|
return await EditAnyCollection(orgId) || await DeleteAnyCollection(orgId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> EditAssignedCollections(Guid orgId)
|
public async Task<bool> EditAssignedCollections(Guid orgId)
|
||||||
|
@ -7,5 +7,5 @@ public interface ICollectionService
|
|||||||
{
|
{
|
||||||
Task SaveAsync(Collection collection, IEnumerable<CollectionAccessSelection> groups = null, IEnumerable<CollectionAccessSelection> users = null, Guid? assignUserId = null);
|
Task SaveAsync(Collection collection, IEnumerable<CollectionAccessSelection> groups = null, IEnumerable<CollectionAccessSelection> users = null, Guid? assignUserId = null);
|
||||||
Task DeleteUserAsync(Collection collection, Guid organizationUserId);
|
Task DeleteUserAsync(Collection collection, Guid organizationUserId);
|
||||||
Task<IEnumerable<Collection>> GetOrganizationCollections(Guid organizationId);
|
Task<IEnumerable<Collection>> GetOrganizationCollectionsAsync(Guid organizationId);
|
||||||
}
|
}
|
||||||
|
@ -96,9 +96,9 @@ public class CollectionService : ICollectionService
|
|||||||
await _eventService.LogOrganizationUserEventAsync(orgUser, Enums.EventType.OrganizationUser_Updated);
|
await _eventService.LogOrganizationUserEventAsync(orgUser, Enums.EventType.OrganizationUser_Updated);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<Collection>> GetOrganizationCollections(Guid organizationId)
|
public async Task<IEnumerable<Collection>> 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();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
@ -81,7 +81,7 @@ public class IconsController : Controller
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
icon = result.Icon;
|
icon = result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only cache not found and smaller images (<= 50kb)
|
// Only cache not found and smaller images (<= 50kb)
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
<PropertyGroup Condition=" '$(RunConfiguration)' == 'Icons' " />
|
<PropertyGroup Condition=" '$(RunConfiguration)' == 'Icons' " />
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AngleSharp" Version="0.16.1" />
|
<PackageReference Include="AngleSharp" Version="1.0.4" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
100
src/Icons/Models/DomainIcons.cs
Normal file
100
src/Icons/Models/DomainIcons.cs
Normal file
@ -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<Icon>
|
||||||
|
{
|
||||||
|
private readonly ILogger<IIconFetchingService> _logger;
|
||||||
|
private readonly IHttpClientFactory _httpClientFactory;
|
||||||
|
private readonly IUriService _uriService;
|
||||||
|
private readonly List<Icon> _icons = new();
|
||||||
|
|
||||||
|
public string Domain { get; }
|
||||||
|
public Icon this[int i]
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _icons[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public IEnumerator<Icon> GetEnumerator() => ((IEnumerable<Icon>)_icons).GetEnumerator();
|
||||||
|
IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_icons).GetEnumerator();
|
||||||
|
|
||||||
|
private DomainIcons(string domain, ILogger<IIconFetchingService> logger, IHttpClientFactory httpClientFactory, IUriService uriService)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_httpClientFactory = httpClientFactory;
|
||||||
|
_uriService = uriService;
|
||||||
|
Domain = domain;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<DomainIcons> FetchAsync(string domain, ILogger<IIconFetchingService> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
110
src/Icons/Models/IconHttpRequest.cs
Normal file
110
src/Icons/Models/IconHttpRequest.cs
Normal file
@ -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<IIconFetchingService> _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<IIconFetchingService> 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<IconHttpResponse> FetchAsync(Uri uri, ILogger<IIconFetchingService> 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<HttpResponseMessage> 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<HttpResponseMessage> 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<HttpResponseMessage> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
72
src/Icons/Models/IconHttpResponse.cs
Normal file
72
src/Icons/Models/IconHttpResponse.cs
Normal file
@ -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<IIconFetchingService> _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<IIconFetchingService> logger, IHttpClientFactory httpClientFactory, IUriService uriService)
|
||||||
|
{
|
||||||
|
_response = response;
|
||||||
|
_logger = logger;
|
||||||
|
_httpClientFactory = httpClientFactory;
|
||||||
|
_uriService = uriService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<Icon>> 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<Icon>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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<IconLink>();
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
220
src/Icons/Models/IconLink.cs
Normal file
220
src/Icons/Models/IconLink.cs
Normal file
@ -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<string> _iconRels = new(StringComparer.InvariantCultureIgnoreCase) { "icon", "apple-touch-icon", "shortcut icon" };
|
||||||
|
private static readonly HashSet<string> _blocklistedRels = new(StringComparer.InvariantCultureIgnoreCase) { "preload", "image_src", "preconnect", "canonical", "alternate", "stylesheet" };
|
||||||
|
private static readonly HashSet<string> _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<string> _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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fetches the icon from the Href. Will always fail unless first validated with IsUsable().
|
||||||
|
/// </summary>
|
||||||
|
public async Task<Icon?> FetchAsync(ILogger<IIconFetchingService> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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; }
|
|
||||||
}
|
|
52
src/Icons/Models/IconUri.cs
Normal file
52
src/Icons/Models/IconUri.cs
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents an ip-validated Uri for use in grabbing an icon.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="uriString"></param>
|
||||||
|
/// <param name="ip"></param>
|
||||||
|
public IconUri(Uri uri, IPAddress ip)
|
||||||
|
{
|
||||||
|
_ip = ip;
|
||||||
|
InnerUri = uri.ChangeHost(_ip.ToString());
|
||||||
|
Host = uri.Host;
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,10 @@
|
|||||||
using Bit.Icons.Models;
|
#nullable enable
|
||||||
|
|
||||||
|
using Bit.Icons.Models;
|
||||||
|
|
||||||
namespace Bit.Icons.Services;
|
namespace Bit.Icons.Services;
|
||||||
|
|
||||||
public interface IIconFetchingService
|
public interface IIconFetchingService
|
||||||
{
|
{
|
||||||
Task<IconResult> GetIconAsync(string domain);
|
Task<Icon?> GetIconAsync(string domain);
|
||||||
}
|
}
|
||||||
|
12
src/Icons/Services/IUriService.cs
Normal file
12
src/Icons/Services/IUriService.cs
Normal file
@ -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);
|
||||||
|
}
|
@ -1,449 +1,47 @@
|
|||||||
using System.Net;
|
#nullable enable
|
||||||
using System.Text;
|
|
||||||
using AngleSharp.Html.Parser;
|
using AngleSharp.Html.Parser;
|
||||||
|
using Bit.Icons.Extensions;
|
||||||
using Bit.Icons.Models;
|
using Bit.Icons.Models;
|
||||||
|
|
||||||
namespace Bit.Icons.Services;
|
namespace Bit.Icons.Services;
|
||||||
|
|
||||||
public class IconFetchingService : IIconFetchingService
|
public class IconFetchingService : IIconFetchingService
|
||||||
{
|
{
|
||||||
private readonly HashSet<string> _iconRels =
|
private readonly IHttpClientFactory _httpClientFactory;
|
||||||
new HashSet<string> { "icon", "apple-touch-icon", "shortcut icon" };
|
|
||||||
private readonly HashSet<string> _blacklistedRels =
|
|
||||||
new HashSet<string> { "preload", "image_src", "preconnect", "canonical", "alternate", "stylesheet" };
|
|
||||||
private readonly HashSet<string> _iconExtensions =
|
|
||||||
new HashSet<string> { ".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<string> _allowedMediaTypes;
|
|
||||||
private readonly HttpClient _httpClient;
|
|
||||||
private readonly ILogger<IIconFetchingService> _logger;
|
private readonly ILogger<IIconFetchingService> _logger;
|
||||||
|
private readonly IHtmlParser _parser;
|
||||||
|
private readonly IUriService _uriService;
|
||||||
|
|
||||||
public IconFetchingService(ILogger<IIconFetchingService> logger)
|
public IconFetchingService(ILogger<IIconFetchingService> logger, IHttpClientFactory httpClientFactory, IHtmlParser parser, IUriService uriService)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_allowedMediaTypes = new HashSet<string>
|
_httpClientFactory = httpClientFactory;
|
||||||
|
_parser = parser;
|
||||||
|
_uriService = uriService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Icon?> 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<Icon?> GetFaviconAsync(string domain)
|
||||||
|
{
|
||||||
|
// Fall back to favicon
|
||||||
|
var faviconUriBuilder = new UriBuilder
|
||||||
{
|
{
|
||||||
_pngMediaType,
|
Scheme = "https",
|
||||||
_icoMediaType,
|
Host = domain,
|
||||||
_icoAltMediaType,
|
Path = "/favicon.ico"
|
||||||
_jpegMediaType
|
|
||||||
};
|
};
|
||||||
|
|
||||||
_httpClient = new HttpClient(new HttpClientHandler
|
if (faviconUriBuilder.TryBuild(out var faviconUri))
|
||||||
{
|
{
|
||||||
AllowAutoRedirect = false,
|
return await new IconLink(faviconUri!).FetchAsync(_logger, _httpClientFactory, _uriService);
|
||||||
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
|
|
||||||
});
|
|
||||||
_httpClient.Timeout = TimeSpan.FromSeconds(20);
|
|
||||||
_httpClient.MaxResponseContentBufferSize = 5000000; // 5 MB
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IconResult> GetIconAsync(string domain)
|
|
||||||
{
|
|
||||||
if (IPAddress.TryParse(domain, out _))
|
|
||||||
{
|
|
||||||
_logger.LogWarning("IP address: {0}.", domain);
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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<IconResult>();
|
|
||||||
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<Task>();
|
|
||||||
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<IconResult> 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<HttpResponseMessage> GetAndFollowAsync(Uri uri, int maxRedirectCount)
|
|
||||||
{
|
|
||||||
var response = await GetAsync(uri);
|
|
||||||
if (response == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return await FollowRedirectsAsync(response, maxRedirectCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<HttpResponseMessage> 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<HttpResponseMessage> 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;
|
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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
109
src/Icons/Services/UriService.cs
Normal file
109
src/Icons/Services/UriService.cs
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Bit.Icons.Services;
|
using Bit.Icons.Extensions;
|
||||||
using Bit.SharedWeb.Utilities;
|
using Bit.SharedWeb.Utilities;
|
||||||
using Microsoft.Net.Http.Headers;
|
using Microsoft.Net.Http.Headers;
|
||||||
|
|
||||||
@ -30,6 +30,12 @@ public class Startup
|
|||||||
ConfigurationBinder.Bind(Configuration.GetSection("IconsSettings"), iconsSettings);
|
ConfigurationBinder.Bind(Configuration.GetSection("IconsSettings"), iconsSettings);
|
||||||
services.AddSingleton(s => iconsSettings);
|
services.AddSingleton(s => iconsSettings);
|
||||||
|
|
||||||
|
// Http client
|
||||||
|
services.ConfigureHttpClients();
|
||||||
|
|
||||||
|
// Add HtmlParser
|
||||||
|
services.AddHtmlParsing();
|
||||||
|
|
||||||
// Cache
|
// Cache
|
||||||
services.AddMemoryCache(options =>
|
services.AddMemoryCache(options =>
|
||||||
{
|
{
|
||||||
@ -37,8 +43,7 @@ public class Startup
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
services.AddSingleton<IDomainMappingService, DomainMappingService>();
|
services.AddServices();
|
||||||
services.AddSingleton<IIconFetchingService, IconFetchingService>();
|
|
||||||
|
|
||||||
// Mvc
|
// Mvc
|
||||||
services.AddMvc();
|
services.AddMvc();
|
||||||
|
42
src/Icons/Util/IPAddressExtension.cs
Normal file
42
src/Icons/Util/IPAddressExtension.cs
Normal file
@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
44
src/Icons/Util/ServiceCollectionExtension.cs
Normal file
44
src/Icons/Util/ServiceCollectionExtension.cs
Normal file
@ -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<IHtmlParser, HtmlParser>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void AddServices(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddSingleton<IUriService, UriService>();
|
||||||
|
services.AddSingleton<IDomainMappingService, DomainMappingService>();
|
||||||
|
services.AddSingleton<IIconFetchingService, IconFetchingService>();
|
||||||
|
}
|
||||||
|
}
|
20
src/Icons/Util/UriBuilderExtension.cs
Normal file
20
src/Icons/Util/UriBuilderExtension.cs
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
41
src/Icons/Util/UriExtension.cs
Normal file
41
src/Icons/Util/UriExtension.cs
Normal file
@ -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<string> paths)
|
||||||
|
{
|
||||||
|
if (!paths.Any())
|
||||||
|
{
|
||||||
|
return uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Uri.TryCreate(uri, paths.First(), out var newUri))
|
||||||
|
{
|
||||||
|
return newUri.ConcatPath(paths.Skip(1));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return uri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -4,12 +4,11 @@
|
|||||||
"net6.0": {
|
"net6.0": {
|
||||||
"AngleSharp": {
|
"AngleSharp": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[0.16.1, )",
|
"requested": "[1.0.4, )",
|
||||||
"resolved": "0.16.1",
|
"resolved": "1.0.4",
|
||||||
"contentHash": "1k7Vbfmr5IUsGaR0QJwTe8XF9zacFUIoWxMgI4X/ipiyKxCWZJZoaG96fNEugL90iubvboRvE1IxuBPibET/Rg==",
|
"contentHash": "G8R4C2MEDFQvjUbYz1QIcGfibjsTJnzP0aWy8iQgWWk7eKacYydCNGD3JMhVL0Q5pZQ9RYlqpKNESEU5NpqsRw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"System.Buffers": "4.5.1",
|
"System.Text.Encoding.CodePages": "6.0.0"
|
||||||
"System.Text.Encoding.CodePages": "5.0.0"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"AspNetCoreRateLimit": {
|
"AspNetCoreRateLimit": {
|
||||||
@ -2379,10 +2378,10 @@
|
|||||||
},
|
},
|
||||||
"System.Text.Encoding.CodePages": {
|
"System.Text.Encoding.CodePages": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "5.0.0",
|
"resolved": "6.0.0",
|
||||||
"contentHash": "NyscU59xX6Uo91qvhOs2Ccho3AR2TnZPomo1Z0K6YpyztBPM/A5VbkzOO19sy3A3i1TtEnTxA7bCe3Us+r5MWg==",
|
"contentHash": "ZFCILZuOvtKPauZ/j/swhvw68ZRi9ATCfvGbk1QfydmcXBkIWecWKn/250UH7rahZ5OoDBaiAudJtPvLwzw85A==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.NETCore.Platforms": "5.0.0"
|
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"System.Text.Encoding.Extensions": {
|
"System.Text.Encoding.Extensions": {
|
||||||
|
@ -10,6 +10,7 @@ using Bit.Core.Enums;
|
|||||||
using Bit.Core.SecretsManager.Entities;
|
using Bit.Core.SecretsManager.Entities;
|
||||||
using Bit.Core.SecretsManager.Repositories;
|
using Bit.Core.SecretsManager.Repositories;
|
||||||
using Bit.Test.Common.Helpers;
|
using Bit.Test.Common.Helpers;
|
||||||
|
using Pipelines.Sockets.Unofficial.Arenas;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Bit.Api.IntegrationTest.SecretsManager.Controllers;
|
namespace Bit.Api.IntegrationTest.SecretsManager.Controllers;
|
||||||
@ -295,6 +296,25 @@ public class ProjectsControllerTests : IClassFixture<ApiApplicationFactory>, IAs
|
|||||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
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]
|
[Theory]
|
||||||
[InlineData(PermissionType.RunAsAdmin)]
|
[InlineData(PermissionType.RunAsAdmin)]
|
||||||
[InlineData(PermissionType.RunAsUserWithPermission)]
|
[InlineData(PermissionType.RunAsUserWithPermission)]
|
||||||
|
@ -171,7 +171,7 @@ public class CollectionsControllerTests
|
|||||||
.Returns(user.Id);
|
.Returns(user.Id);
|
||||||
|
|
||||||
sutProvider.GetDependency<ICollectionService>()
|
sutProvider.GetDependency<ICollectionService>()
|
||||||
.GetOrganizationCollections(orgId)
|
.GetOrganizationCollectionsAsync(orgId)
|
||||||
.Returns(collections);
|
.Returns(collections);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@ -237,7 +237,7 @@ public class CollectionsControllerTests
|
|||||||
.Returns(user.Id);
|
.Returns(user.Id);
|
||||||
|
|
||||||
sutProvider.GetDependency<ICollectionService>()
|
sutProvider.GetDependency<ICollectionService>()
|
||||||
.GetOrganizationCollections(orgId)
|
.GetOrganizationCollectionsAsync(orgId)
|
||||||
.Returns(collections);
|
.Returns(collections);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
@ -1181,15 +1181,6 @@
|
|||||||
"System.Security.Cryptography.Pkcs": "6.0.0"
|
"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": {
|
"MySqlConnector": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "2.2.5",
|
"resolved": "2.2.5",
|
||||||
@ -3015,129 +3006,128 @@
|
|||||||
"api": {
|
"api": {
|
||||||
"type": "Project",
|
"type": "Project",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"AspNetCore.HealthChecks.AzureServiceBus": "6.1.0",
|
"AspNetCore.HealthChecks.AzureServiceBus": "[6.1.0, )",
|
||||||
"AspNetCore.HealthChecks.AzureStorage": "6.1.2",
|
"AspNetCore.HealthChecks.AzureStorage": "[6.1.2, )",
|
||||||
"AspNetCore.HealthChecks.Network": "6.0.4",
|
"AspNetCore.HealthChecks.Network": "[6.0.4, )",
|
||||||
"AspNetCore.HealthChecks.Redis": "6.0.4",
|
"AspNetCore.HealthChecks.Redis": "[6.0.4, )",
|
||||||
"AspNetCore.HealthChecks.SendGrid": "6.0.2",
|
"AspNetCore.HealthChecks.SendGrid": "[6.0.2, )",
|
||||||
"AspNetCore.HealthChecks.SqlServer": "6.0.2",
|
"AspNetCore.HealthChecks.SqlServer": "[6.0.2, )",
|
||||||
"AspNetCore.HealthChecks.Uris": "6.0.3",
|
"AspNetCore.HealthChecks.Uris": "[6.0.3, )",
|
||||||
"Azure.Messaging.EventGrid": "4.10.0",
|
"Azure.Messaging.EventGrid": "[4.10.0, )",
|
||||||
"Commercial.Core": "2023.7.2",
|
"Commercial.Core": "[2023.7.2, )",
|
||||||
"Commercial.Infrastructure.EntityFramework": "2023.7.2",
|
"Commercial.Infrastructure.EntityFramework": "[2023.7.2, )",
|
||||||
"Core": "2023.7.2",
|
"Core": "[2023.7.2, )",
|
||||||
"SharedWeb": "2023.7.2",
|
"SharedWeb": "[2023.7.2, )",
|
||||||
"Swashbuckle.AspNetCore": "6.5.0"
|
"Swashbuckle.AspNetCore": "[6.5.0, )"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"commercial.core": {
|
"commercial.core": {
|
||||||
"type": "Project",
|
"type": "Project",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Core": "2023.7.2"
|
"Core": "[2023.7.2, )"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"commercial.infrastructure.entityframework": {
|
"commercial.infrastructure.entityframework": {
|
||||||
"type": "Project",
|
"type": "Project",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"AutoMapper.Extensions.Microsoft.DependencyInjection": "12.0.1",
|
"AutoMapper.Extensions.Microsoft.DependencyInjection": "[12.0.1, )",
|
||||||
"Core": "2023.7.2",
|
"Core": "[2023.7.2, )",
|
||||||
"Infrastructure.EntityFramework": "2023.7.2"
|
"Infrastructure.EntityFramework": "[2023.7.2, )"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"type": "Project",
|
"type": "Project",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"AutoFixture.AutoNSubstitute": "4.17.0",
|
"AutoFixture.AutoNSubstitute": "[4.17.0, )",
|
||||||
"AutoFixture.Xunit2": "4.17.0",
|
"AutoFixture.Xunit2": "[4.17.0, )",
|
||||||
"Core": "2023.7.2",
|
"Core": "[2023.7.2, )",
|
||||||
"Kralizek.AutoFixture.Extensions.MockHttp": "1.2.0",
|
"Kralizek.AutoFixture.Extensions.MockHttp": "[1.2.0, )",
|
||||||
"Microsoft.NET.Test.Sdk": "17.1.0",
|
"Microsoft.NET.Test.Sdk": "[17.1.0, )",
|
||||||
"NSubstitute": "4.3.0",
|
"NSubstitute": "[4.3.0, )",
|
||||||
"xunit": "2.4.1"
|
"xunit": "[2.4.1, )"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"core": {
|
"core": {
|
||||||
"type": "Project",
|
"type": "Project",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"AWSSDK.SQS": "3.7.2.47",
|
"AWSSDK.SQS": "[3.7.2.47, )",
|
||||||
"AWSSDK.SimpleEmail": "3.7.0.150",
|
"AWSSDK.SimpleEmail": "[3.7.0.150, )",
|
||||||
"AspNetCoreRateLimit": "4.0.2",
|
"AspNetCoreRateLimit": "[4.0.2, )",
|
||||||
"AspNetCoreRateLimit.Redis": "1.0.1",
|
"AspNetCoreRateLimit.Redis": "[1.0.1, )",
|
||||||
"Azure.Extensions.AspNetCore.DataProtection.Blobs": "1.3.2",
|
"Azure.Extensions.AspNetCore.DataProtection.Blobs": "[1.3.2, )",
|
||||||
"Azure.Messaging.ServiceBus": "7.15.0",
|
"Azure.Messaging.ServiceBus": "[7.15.0, )",
|
||||||
"Azure.Storage.Blobs": "12.14.1",
|
"Azure.Storage.Blobs": "[12.14.1, )",
|
||||||
"Azure.Storage.Queues": "12.12.0",
|
"Azure.Storage.Queues": "[12.12.0, )",
|
||||||
"BitPay.Light": "1.0.1907",
|
"BitPay.Light": "[1.0.1907, )",
|
||||||
"Braintree": "5.12.0",
|
"Braintree": "[5.12.0, )",
|
||||||
"DnsClient": "1.7.0",
|
"DnsClient": "[1.7.0, )",
|
||||||
"Fido2.AspNet": "3.0.1",
|
"Fido2.AspNet": "[3.0.1, )",
|
||||||
"Handlebars.Net": "2.1.2",
|
"Handlebars.Net": "[2.1.2, )",
|
||||||
"IdentityServer4": "4.1.2",
|
"IdentityServer4": "[4.1.2, )",
|
||||||
"IdentityServer4.AccessTokenValidation": "3.0.1",
|
"IdentityServer4.AccessTokenValidation": "[3.0.1, )",
|
||||||
"LaunchDarkly.ServerSdk": "7.0.0",
|
"LaunchDarkly.ServerSdk": "[7.0.0, )",
|
||||||
"MailKit": "3.2.0",
|
"MailKit": "[3.2.0, )",
|
||||||
"Microsoft.AspNetCore.Authentication.JwtBearer": "6.0.4",
|
"Microsoft.AspNetCore.Authentication.JwtBearer": "[6.0.4, )",
|
||||||
"Microsoft.Azure.Cosmos.Table": "1.0.8",
|
"Microsoft.Azure.Cosmos.Table": "[1.0.8, )",
|
||||||
"Microsoft.Azure.NotificationHubs": "4.1.0",
|
"Microsoft.Azure.NotificationHubs": "[4.1.0, )",
|
||||||
"Microsoft.Data.SqlClient": "5.0.1",
|
"Microsoft.Data.SqlClient": "[5.0.1, )",
|
||||||
"Microsoft.Extensions.Caching.StackExchangeRedis": "6.0.6",
|
"Microsoft.Extensions.Caching.StackExchangeRedis": "[6.0.6, )",
|
||||||
"Microsoft.Extensions.Configuration.EnvironmentVariables": "6.0.1",
|
"Microsoft.Extensions.Configuration.EnvironmentVariables": "[6.0.1, )",
|
||||||
"Microsoft.Extensions.Configuration.UserSecrets": "6.0.1",
|
"Microsoft.Extensions.Configuration.UserSecrets": "[6.0.1, )",
|
||||||
"Microsoft.Extensions.Identity.Stores": "6.0.4",
|
"Microsoft.Extensions.Identity.Stores": "[6.0.4, )",
|
||||||
"Newtonsoft.Json": "13.0.1",
|
"Newtonsoft.Json": "[13.0.1, )",
|
||||||
"Otp.NET": "1.2.2",
|
"Otp.NET": "[1.2.2, )",
|
||||||
"Quartz": "3.4.0",
|
"Quartz": "[3.4.0, )",
|
||||||
"SendGrid": "9.27.0",
|
"SendGrid": "[9.27.0, )",
|
||||||
"Sentry.Serilog": "3.16.0",
|
"Sentry.Serilog": "[3.16.0, )",
|
||||||
"Serilog.AspNetCore": "5.0.0",
|
"Serilog.AspNetCore": "[5.0.0, )",
|
||||||
"Serilog.Extensions.Logging": "3.1.0",
|
"Serilog.Extensions.Logging": "[3.1.0, )",
|
||||||
"Serilog.Extensions.Logging.File": "2.0.0",
|
"Serilog.Extensions.Logging.File": "[2.0.0, )",
|
||||||
"Serilog.Sinks.AzureCosmosDB": "2.0.0",
|
"Serilog.Sinks.AzureCosmosDB": "[2.0.0, )",
|
||||||
"Serilog.Sinks.SyslogMessages": "2.0.6",
|
"Serilog.Sinks.SyslogMessages": "[2.0.6, )",
|
||||||
"Stripe.net": "40.0.0",
|
"Stripe.net": "[40.0.0, )",
|
||||||
"YubicoDotNetClient": "1.2.0"
|
"YubicoDotNetClient": "[1.2.0, )"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"core.test": {
|
"core.test": {
|
||||||
"type": "Project",
|
"type": "Project",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"AutoFixture.AutoNSubstitute": "4.17.0",
|
"AutoFixture.AutoNSubstitute": "[4.17.0, )",
|
||||||
"AutoFixture.Xunit2": "4.17.0",
|
"AutoFixture.Xunit2": "[4.17.0, )",
|
||||||
"Common": "2023.7.2",
|
"Common": "[2023.7.2, )",
|
||||||
"Core": "2023.7.2",
|
"Core": "[2023.7.2, )",
|
||||||
"Kralizek.AutoFixture.Extensions.MockHttp": "1.2.0",
|
"Kralizek.AutoFixture.Extensions.MockHttp": "[1.2.0, )",
|
||||||
"Microsoft.NET.Test.Sdk": "17.1.0",
|
"Microsoft.NET.Test.Sdk": "[17.1.0, )",
|
||||||
"Moq": "4.17.2",
|
"NSubstitute": "[4.3.0, )",
|
||||||
"NSubstitute": "4.3.0",
|
"xunit": "[2.4.1, )"
|
||||||
"xunit": "2.4.1"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"infrastructure.dapper": {
|
"infrastructure.dapper": {
|
||||||
"type": "Project",
|
"type": "Project",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Core": "2023.7.2",
|
"Core": "[2023.7.2, )",
|
||||||
"Dapper": "2.0.123"
|
"Dapper": "[2.0.123, )"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"infrastructure.entityframework": {
|
"infrastructure.entityframework": {
|
||||||
"type": "Project",
|
"type": "Project",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"AutoMapper.Extensions.Microsoft.DependencyInjection": "12.0.1",
|
"AutoMapper.Extensions.Microsoft.DependencyInjection": "[12.0.1, )",
|
||||||
"Core": "2023.7.2",
|
"Core": "[2023.7.2, )",
|
||||||
"Microsoft.EntityFrameworkCore.Relational": "7.0.5",
|
"Microsoft.EntityFrameworkCore.Relational": "[7.0.5, )",
|
||||||
"Microsoft.EntityFrameworkCore.SqlServer": "7.0.5",
|
"Microsoft.EntityFrameworkCore.SqlServer": "[7.0.5, )",
|
||||||
"Microsoft.EntityFrameworkCore.Sqlite": "7.0.5",
|
"Microsoft.EntityFrameworkCore.Sqlite": "[7.0.5, )",
|
||||||
"Npgsql.EntityFrameworkCore.PostgreSQL": "7.0.4",
|
"Npgsql.EntityFrameworkCore.PostgreSQL": "[7.0.4, )",
|
||||||
"Pomelo.EntityFrameworkCore.MySql": "7.0.0",
|
"Pomelo.EntityFrameworkCore.MySql": "[7.0.0, )",
|
||||||
"linq2db.EntityFrameworkCore": "7.5.0"
|
"linq2db.EntityFrameworkCore": "[7.5.0, )"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sharedweb": {
|
"sharedweb": {
|
||||||
"type": "Project",
|
"type": "Project",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Core": "2023.7.2",
|
"Core": "[2023.7.2, )",
|
||||||
"Infrastructure.Dapper": "2023.7.2",
|
"Infrastructure.Dapper": "[2023.7.2, )",
|
||||||
"Infrastructure.EntityFramework": "2023.7.2"
|
"Infrastructure.EntityFramework": "[2023.7.2, )"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
59
test/Common/Helpers/HtmlBuilder.cs
Normal file
59
test/Common/Helpers/HtmlBuilder.cs
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
104
test/Common/MockedHttpClient/HttpRequestMatcher.cs
Normal file
104
test/Common/MockedHttpClient/HttpRequestMatcher.cs
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using System.Net;
|
||||||
|
|
||||||
|
namespace Bit.Test.Common.MockedHttpClient;
|
||||||
|
|
||||||
|
public class HttpRequestMatcher : IHttpRequestMatcher
|
||||||
|
{
|
||||||
|
private readonly Func<HttpRequestMessage, bool> _matcher;
|
||||||
|
private HttpRequestMatcher? _childMatcher;
|
||||||
|
private MockedHttpResponse _mockedResponse = new(HttpStatusCode.OK);
|
||||||
|
private bool _responseSpecified = false;
|
||||||
|
|
||||||
|
public int NumberOfMatches { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns whether or not the provided request can be handled by this matcher chain.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
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<HttpRequestMessage, bool> 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<string, string> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configure how this matcher should respond to matching HttpRequestMessages.
|
||||||
|
/// Note, after specifying a response, you can no longer further specify match criteria.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="statusCode"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public MockedHttpResponse RespondWith(HttpStatusCode statusCode)
|
||||||
|
{
|
||||||
|
_responseSpecified = true;
|
||||||
|
_mockedResponse = new MockedHttpResponse(statusCode);
|
||||||
|
return _mockedResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called to produce an HttpResponseMessage for the given request. This is probably something you want to leave alone
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request"></param>
|
||||||
|
public async Task<HttpResponseMessage> RespondToAsync(HttpRequestMessage request)
|
||||||
|
{
|
||||||
|
NumberOfMatches++;
|
||||||
|
return await (_childMatcher == null ? _mockedResponse.RespondToAsync(request) : _childMatcher.RespondToAsync(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpRequestMatcher AddChild(Func<HttpRequestMessage, bool> matcher)
|
||||||
|
{
|
||||||
|
if (_responseSpecified)
|
||||||
|
{
|
||||||
|
throw new Exception("Cannot continue to configure a matcher after a response has been specified");
|
||||||
|
}
|
||||||
|
_childMatcher = new HttpRequestMatcher(matcher);
|
||||||
|
return _childMatcher;
|
||||||
|
}
|
||||||
|
}
|
84
test/Common/MockedHttpClient/HttpResponseBuilder.cs
Normal file
84
test/Common/MockedHttpClient/HttpResponseBuilder.cs
Normal file
@ -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<KeyValuePair<string, string>> Headers { get; set; } = new List<KeyValuePair<string, string>>();
|
||||||
|
public IEnumerable<string> HeadersToRemove { get; set; } = new List<string>();
|
||||||
|
public HttpContent Content { get; set; }
|
||||||
|
|
||||||
|
public async Task<HttpResponseMessage> 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<string, string>(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);
|
||||||
|
}
|
||||||
|
}
|
10
test/Common/MockedHttpClient/IHttpRequestMatcher.cs
Normal file
10
test/Common/MockedHttpClient/IHttpRequestMatcher.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace Bit.Test.Common.MockedHttpClient;
|
||||||
|
|
||||||
|
public interface IHttpRequestMatcher
|
||||||
|
{
|
||||||
|
int NumberOfMatches { get; }
|
||||||
|
bool Matches(HttpRequestMessage request);
|
||||||
|
Task<HttpResponseMessage> RespondToAsync(HttpRequestMessage request);
|
||||||
|
}
|
7
test/Common/MockedHttpClient/IMockedHttpResponse.cs
Normal file
7
test/Common/MockedHttpClient/IMockedHttpResponse.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
namespace Bit.Test.Common.MockedHttpClient;
|
||||||
|
|
||||||
|
public interface IMockedHttpResponse
|
||||||
|
{
|
||||||
|
int NumberOfResponses { get; }
|
||||||
|
Task<HttpResponseMessage> RespondToAsync(HttpRequestMessage request);
|
||||||
|
}
|
113
test/Common/MockedHttpClient/MockedHttpMessageHandler.cs
Normal file
113
test/Common/MockedHttpClient/MockedHttpMessageHandler.cs
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using System.Net;
|
||||||
|
|
||||||
|
namespace Bit.Test.Common.MockedHttpClient;
|
||||||
|
|
||||||
|
public class MockedHttpMessageHandler : HttpMessageHandler
|
||||||
|
{
|
||||||
|
private readonly List<IHttpRequestMatcher> _matchers = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The fallback handler to use when the request does not match any of the provided matchers.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A Matcher that responds with 404 Not Found</returns>
|
||||||
|
public MockedHttpResponse Fallback { get; set; } = new(HttpStatusCode.NotFound);
|
||||||
|
|
||||||
|
protected override async Task<HttpResponseMessage> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Instantiates a new HttpRequestMessage matcher that will handle requests in fitting with the returned matcher. Configuration can be chained.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="requestMatcher"></param>
|
||||||
|
/// <typeparam name="T"></typeparam>
|
||||||
|
/// <returns></returns>
|
||||||
|
public T When<T>(T requestMatcher) where T : IHttpRequestMatcher
|
||||||
|
{
|
||||||
|
_matchers.Add(requestMatcher);
|
||||||
|
return requestMatcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Instantiates a new HttpRequestMessage matcher that will handle requests in fitting with the returned matcher. Configuration can be chained.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="requestMatcher"></param>
|
||||||
|
/// <typeparam name="T"></typeparam>
|
||||||
|
/// <returns></returns>
|
||||||
|
public HttpRequestMatcher When(string uri)
|
||||||
|
{
|
||||||
|
var matcher = new HttpRequestMatcher(uri);
|
||||||
|
_matchers.Add(matcher);
|
||||||
|
return matcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Instantiates a new HttpRequestMessage matcher that will handle requests in fitting with the returned matcher. Configuration can be chained.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="requestMatcher"></param>
|
||||||
|
/// <typeparam name="T"></typeparam>
|
||||||
|
/// <returns></returns>
|
||||||
|
public HttpRequestMatcher When(Uri uri)
|
||||||
|
{
|
||||||
|
var matcher = new HttpRequestMatcher(uri);
|
||||||
|
_matchers.Add(matcher);
|
||||||
|
return matcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Instantiates a new HttpRequestMessage matcher that will handle requests in fitting with the returned matcher. Configuration can be chained.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="requestMatcher"></param>
|
||||||
|
/// <typeparam name="T"></typeparam>
|
||||||
|
/// <returns></returns>
|
||||||
|
public HttpRequestMatcher When(HttpMethod method)
|
||||||
|
{
|
||||||
|
var matcher = new HttpRequestMatcher(method);
|
||||||
|
_matchers.Add(matcher);
|
||||||
|
return matcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Instantiates a new HttpRequestMessage matcher that will handle requests in fitting with the returned matcher. Configuration can be chained.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="requestMatcher"></param>
|
||||||
|
/// <typeparam name="T"></typeparam>
|
||||||
|
/// <returns></returns>
|
||||||
|
public HttpRequestMatcher When(HttpMethod method, string uri)
|
||||||
|
{
|
||||||
|
var matcher = new HttpRequestMatcher(method, uri);
|
||||||
|
_matchers.Add(matcher);
|
||||||
|
return matcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Instantiates a new HttpRequestMessage matcher that will handle requests in fitting with the returned matcher. Configuration can be chained.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="requestMatcher"></param>
|
||||||
|
/// <typeparam name="T"></typeparam>
|
||||||
|
/// <returns></returns>
|
||||||
|
public HttpRequestMatcher When(Func<HttpRequestMessage, bool> matcher)
|
||||||
|
{
|
||||||
|
var requestMatcher = new HttpRequestMatcher(matcher);
|
||||||
|
_matchers.Add(requestMatcher);
|
||||||
|
return requestMatcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts the MockedHttpMessageHandler to a HttpClient that can be used in your tests after setup.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public HttpClient ToHttpClient()
|
||||||
|
{
|
||||||
|
return new HttpClient(this);
|
||||||
|
}
|
||||||
|
}
|
68
test/Common/MockedHttpClient/MockedHttpResponse.cs
Normal file
68
test/Common/MockedHttpClient/MockedHttpResponse.cs
Normal file
@ -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<HttpRequestMessage, HttpResponseBuilder, HttpResponseBuilder> _responder;
|
||||||
|
|
||||||
|
public int NumberOfResponses { get; private set; }
|
||||||
|
|
||||||
|
public MockedHttpResponse(HttpStatusCode statusCode)
|
||||||
|
{
|
||||||
|
_responder = (_, builder) => builder.WithStatusCode(statusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private MockedHttpResponse(Func<HttpRequestMessage, HttpResponseBuilder, HttpResponseBuilder> 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<string, string>[] 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<HttpResponseMessage> RespondToAsync(HttpRequestMessage request)
|
||||||
|
{
|
||||||
|
return await RespondToAsync(request, new HttpResponseBuilder());
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<HttpResponseMessage> 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<HttpRequestMessage, HttpResponseBuilder, HttpResponseBuilder> responder)
|
||||||
|
{
|
||||||
|
_childResponse = new MockedHttpResponse(responder);
|
||||||
|
return _childResponse;
|
||||||
|
}
|
||||||
|
}
|
@ -6,7 +6,7 @@ using AutoFixture.Xunit2;
|
|||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.Test.Helpers.Factories;
|
using Bit.Core.Test.Helpers.Factories;
|
||||||
using Microsoft.AspNetCore.DataProtection;
|
using Microsoft.AspNetCore.DataProtection;
|
||||||
using Moq;
|
using NSubstitute;
|
||||||
|
|
||||||
namespace Bit.Test.Common.AutoFixture;
|
namespace Bit.Test.Common.AutoFixture;
|
||||||
|
|
||||||
@ -33,17 +33,17 @@ public class GlobalSettingsBuilder : ISpecimenBuilder
|
|||||||
|
|
||||||
if (pi.ParameterType == typeof(IDataProtectionProvider))
|
if (pi.ParameterType == typeof(IDataProtectionProvider))
|
||||||
{
|
{
|
||||||
var dataProtector = new Mock<IDataProtector>();
|
var dataProtector = Substitute.For<IDataProtector>();
|
||||||
dataProtector
|
dataProtector.Unprotect(Arg.Any<byte[]>())
|
||||||
.Setup(d => d.Unprotect(It.IsAny<byte[]>()))
|
.Returns(data =>
|
||||||
.Returns<byte[]>(data => Encoding.UTF8.GetBytes(Constants.DatabaseFieldProtectedPrefix + Encoding.UTF8.GetString(data)));
|
Encoding.UTF8.GetBytes(Constants.DatabaseFieldProtectedPrefix +
|
||||||
|
Encoding.UTF8.GetString((byte[])data[0])));
|
||||||
|
|
||||||
var dataProtectionProvider = new Mock<IDataProtectionProvider>();
|
var dataProtectionProvider = Substitute.For<IDataProtectionProvider>();
|
||||||
dataProtectionProvider
|
dataProtectionProvider.CreateProtector(Constants.DatabaseFieldProtectorPurpose)
|
||||||
.Setup(x => x.CreateProtector(Constants.DatabaseFieldProtectorPurpose))
|
.Returns(dataProtector);
|
||||||
.Returns(dataProtector.Object);
|
|
||||||
|
|
||||||
return dataProtectionProvider.Object;
|
return dataProtectionProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new NoSpecimen();
|
return new NoSpecimen();
|
||||||
|
@ -9,7 +9,6 @@
|
|||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNetTestSdkVersion)" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNetTestSdkVersion)" />
|
||||||
<PackageReference Include="Moq" Version="4.17.2" />
|
|
||||||
<PackageReference Include="NSubstitute" Version="$(NSubstituteVersion)" />
|
<PackageReference Include="NSubstitute" Version="$(NSubstituteVersion)" />
|
||||||
<PackageReference Include="xunit" Version="$(XUnitVersion)" />
|
<PackageReference Include="xunit" Version="$(XUnitVersion)" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="$(XUnitRunnerVisualStudioVersion)">
|
<PackageReference Include="xunit.runner.visualstudio" Version="$(XUnitRunnerVisualStudioVersion)">
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using Bit.Core.Entities;
|
using Bit.Core.Context;
|
||||||
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
@ -185,4 +186,56 @@ public class CollectionServiceTest
|
|||||||
.LogOrganizationUserEventAsync(default, default);
|
.LogOrganizationUserEventAsync(default, default);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task GetOrganizationCollectionsAsync_WithViewAssignedCollectionsTrue_ReturnsAssignedCollections(
|
||||||
|
CollectionDetails collectionDetails, Guid organizationId, Guid userId, SutProvider<CollectionService> sutProvider)
|
||||||
|
{
|
||||||
|
collectionDetails.OrganizationId = organizationId;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
|
||||||
|
sutProvider.GetDependency<ICollectionRepository>()
|
||||||
|
.GetManyByUserIdAsync(userId)
|
||||||
|
.Returns(new List<CollectionDetails> { collectionDetails });
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().ViewAssignedCollections(organizationId).Returns(true);
|
||||||
|
|
||||||
|
var result = await sutProvider.Sut.GetOrganizationCollectionsAsync(organizationId);
|
||||||
|
|
||||||
|
Assert.Single(result);
|
||||||
|
Assert.Equal(collectionDetails, result.First());
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<ICollectionRepository>().DidNotReceiveWithAnyArgs().GetManyByOrganizationIdAsync(default);
|
||||||
|
await sutProvider.GetDependency<ICollectionRepository>().Received(1).GetManyByUserIdAsync(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task GetOrganizationCollectionsAsync_WithViewAllCollectionsTrue_ReturnsAllOrganizationCollections(
|
||||||
|
Collection collection, Guid organizationId, Guid userId, SutProvider<CollectionService> sutProvider)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
|
||||||
|
sutProvider.GetDependency<ICollectionRepository>()
|
||||||
|
.GetManyByOrganizationIdAsync(organizationId)
|
||||||
|
.Returns(new List<Collection> { collection });
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().ViewAssignedCollections(organizationId).Returns(true);
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().ViewAllCollections(organizationId).Returns(true);
|
||||||
|
|
||||||
|
var result = await sutProvider.Sut.GetOrganizationCollectionsAsync(organizationId);
|
||||||
|
|
||||||
|
Assert.Single(result);
|
||||||
|
Assert.Equal(collection, result.First());
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<ICollectionRepository>().Received(1).GetManyByOrganizationIdAsync(organizationId);
|
||||||
|
await sutProvider.GetDependency<ICollectionRepository>().DidNotReceiveWithAnyArgs().GetManyByUserIdAsync(default);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task GetOrganizationCollectionsAsync_WithViewAssignedCollectionsFalse_ThrowsBadRequestException(
|
||||||
|
Guid organizationId, SutProvider<CollectionService> sutProvider)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().ViewAssignedCollections(organizationId).Returns(false);
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetOrganizationCollectionsAsync(organizationId));
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<ICollectionRepository>().DidNotReceiveWithAnyArgs().GetManyByOrganizationIdAsync(default);
|
||||||
|
await sutProvider.GetDependency<ICollectionRepository>().DidNotReceiveWithAnyArgs().GetManyByUserIdAsync(default);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ using Bit.Core.Settings;
|
|||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Moq;
|
using NSubstitute;
|
||||||
using StackExchange.Redis;
|
using StackExchange.Redis;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
@ -37,14 +37,12 @@ public class CustomRedisProcessingStrategyTests
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
private readonly Mock<ICounterKeyBuilder> _mockCounterKeyBuilder = new();
|
private readonly ICounterKeyBuilder _mockCounterKeyBuilder = Substitute.For<ICounterKeyBuilder>();
|
||||||
private Mock<IDatabase> _mockDb;
|
private IDatabase _mockDb;
|
||||||
|
|
||||||
public CustomRedisProcessingStrategyTests()
|
public CustomRedisProcessingStrategyTests()
|
||||||
{
|
{
|
||||||
_mockCounterKeyBuilder
|
_mockCounterKeyBuilder.Build(Arg.Any<ClientRequestIdentity>(), Arg.Any<RateLimitRule>())
|
||||||
.Setup(x =>
|
|
||||||
x.Build(It.IsAny<ClientRequestIdentity>(), It.IsAny<RateLimitRule>()))
|
|
||||||
.Returns(_sampleClientId.ClientId);
|
.Returns(_sampleClientId.ClientId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,12 +53,12 @@ public class CustomRedisProcessingStrategyTests
|
|||||||
var strategy = BuildProcessingStrategy();
|
var strategy = BuildProcessingStrategy();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await strategy.ProcessRequestAsync(_sampleClientId, _sampleRule, _mockCounterKeyBuilder.Object, _sampleOptions,
|
var result = await strategy.ProcessRequestAsync(_sampleClientId, _sampleRule, _mockCounterKeyBuilder, _sampleOptions,
|
||||||
CancellationToken.None);
|
CancellationToken.None);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal(1, result.Count);
|
Assert.Equal(1, result.Count);
|
||||||
VerifyRedisCalls(Times.Once());
|
VerifyRedisCalls(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -70,60 +68,63 @@ public class CustomRedisProcessingStrategyTests
|
|||||||
var strategy = BuildProcessingStrategy(false);
|
var strategy = BuildProcessingStrategy(false);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await strategy.ProcessRequestAsync(_sampleClientId, _sampleRule, _mockCounterKeyBuilder.Object, _sampleOptions,
|
var result = await strategy.ProcessRequestAsync(_sampleClientId, _sampleRule, _mockCounterKeyBuilder, _sampleOptions,
|
||||||
CancellationToken.None);
|
CancellationToken.None);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal(0, result.Count);
|
Assert.Equal(0, result.Count);
|
||||||
VerifyRedisCalls(Times.Never());
|
VerifyRedisNotCalled();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task SkipRateLimit_When_TimeoutThresholdExceeded()
|
public async Task SkipRateLimit_When_TimeoutThresholdExceeded()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var mockCache = new Mock<IMemoryCache>();
|
var mockCache = Substitute.For<IMemoryCache>();
|
||||||
object existingCount = new CustomRedisProcessingStrategy.TimeoutCounter
|
object existingCount = new CustomRedisProcessingStrategy.TimeoutCounter
|
||||||
{
|
{
|
||||||
Count = _sampleSettings.DistributedIpRateLimiting.MaxRedisTimeoutsThreshold + 1
|
Count = _sampleSettings.DistributedIpRateLimiting.MaxRedisTimeoutsThreshold + 1
|
||||||
};
|
};
|
||||||
mockCache.Setup(x => x.TryGetValue(It.IsAny<object>(), out existingCount)).Returns(true);
|
mockCache.TryGetValue(Arg.Any<object>(), out existingCount).ReturnsForAnyArgs(x =>
|
||||||
|
{
|
||||||
|
x[1] = existingCount;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
var strategy = BuildProcessingStrategy(mockCache: mockCache.Object);
|
var strategy = BuildProcessingStrategy(mockCache: mockCache);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await strategy.ProcessRequestAsync(_sampleClientId, _sampleRule, _mockCounterKeyBuilder.Object, _sampleOptions,
|
var result = await strategy.ProcessRequestAsync(_sampleClientId, _sampleRule, _mockCounterKeyBuilder, _sampleOptions,
|
||||||
CancellationToken.None);
|
CancellationToken.None);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal(0, result.Count);
|
Assert.Equal(0, result.Count);
|
||||||
VerifyRedisCalls(Times.Never());
|
VerifyRedisNotCalled();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task SkipRateLimit_When_RedisTimeoutException()
|
public async Task SkipRateLimit_When_RedisTimeoutException()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var mockCache = new Mock<IMemoryCache>();
|
var mockCache = Substitute.For<IMemoryCache>();
|
||||||
var mockCacheEntry = new Mock<ICacheEntry>();
|
var mockCacheEntry = Substitute.For<ICacheEntry>();
|
||||||
mockCacheEntry.SetupAllProperties();
|
mockCache.CreateEntry(Arg.Any<object>()).Returns(mockCacheEntry);
|
||||||
mockCache.Setup(x => x.CreateEntry(It.IsAny<object>())).Returns(mockCacheEntry.Object);
|
|
||||||
|
|
||||||
var strategy = BuildProcessingStrategy(mockCache: mockCache.Object, throwRedisTimeout: true);
|
var strategy = BuildProcessingStrategy(mockCache: mockCache, throwRedisTimeout: true);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await strategy.ProcessRequestAsync(_sampleClientId, _sampleRule, _mockCounterKeyBuilder.Object, _sampleOptions,
|
var result = await strategy.ProcessRequestAsync(_sampleClientId, _sampleRule, _mockCounterKeyBuilder, _sampleOptions,
|
||||||
CancellationToken.None);
|
CancellationToken.None);
|
||||||
|
|
||||||
var timeoutCounter = ((CustomRedisProcessingStrategy.TimeoutCounter)mockCacheEntry.Object.Value);
|
var timeoutCounter = ((CustomRedisProcessingStrategy.TimeoutCounter)mockCacheEntry.Value);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal(0, result.Count); // Skip rate limiting
|
Assert.Equal(0, result.Count); // Skip rate limiting
|
||||||
VerifyRedisCalls(Times.Once());
|
VerifyRedisCalls(1);
|
||||||
|
|
||||||
Assert.Equal(1, timeoutCounter.Count); // Timeout count increased/cached
|
Assert.Equal(1, timeoutCounter.Count); // Timeout count increased/cached
|
||||||
Assert.NotNull(mockCacheEntry.Object.AbsoluteExpiration);
|
Assert.NotNull(mockCacheEntry.AbsoluteExpiration);
|
||||||
mockCache.Verify(x => x.CreateEntry(It.IsAny<object>()));
|
mockCache.Received().CreateEntry(Arg.Any<object>());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -136,26 +137,33 @@ public class CustomRedisProcessingStrategyTests
|
|||||||
// Act
|
// Act
|
||||||
|
|
||||||
// Redis Timeout 1
|
// Redis Timeout 1
|
||||||
await strategy.ProcessRequestAsync(_sampleClientId, _sampleRule, _mockCounterKeyBuilder.Object, _sampleOptions,
|
await strategy.ProcessRequestAsync(_sampleClientId, _sampleRule, _mockCounterKeyBuilder, _sampleOptions,
|
||||||
CancellationToken.None);
|
CancellationToken.None);
|
||||||
|
|
||||||
// Redis Timeout 2
|
// Redis Timeout 2
|
||||||
await strategy.ProcessRequestAsync(_sampleClientId, _sampleRule, _mockCounterKeyBuilder.Object, _sampleOptions,
|
await strategy.ProcessRequestAsync(_sampleClientId, _sampleRule, _mockCounterKeyBuilder, _sampleOptions,
|
||||||
CancellationToken.None);
|
CancellationToken.None);
|
||||||
|
|
||||||
// Skip Redis
|
// Skip Redis
|
||||||
await strategy.ProcessRequestAsync(_sampleClientId, _sampleRule, _mockCounterKeyBuilder.Object, _sampleOptions,
|
await strategy.ProcessRequestAsync(_sampleClientId, _sampleRule, _mockCounterKeyBuilder, _sampleOptions,
|
||||||
CancellationToken.None);
|
CancellationToken.None);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
VerifyRedisCalls(Times.Exactly(_sampleSettings.DistributedIpRateLimiting.MaxRedisTimeoutsThreshold));
|
VerifyRedisCalls(_sampleSettings.DistributedIpRateLimiting.MaxRedisTimeoutsThreshold);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void VerifyRedisCalls(Times times)
|
private void VerifyRedisCalls(int times)
|
||||||
{
|
{
|
||||||
_mockDb.Verify(x =>
|
_mockDb
|
||||||
x.ScriptEvaluateAsync(It.IsAny<LuaScript>(), It.IsAny<object>(), It.IsAny<CommandFlags>()),
|
.Received(times)
|
||||||
times);
|
.ScriptEvaluateAsync(Arg.Any<LuaScript>(), Arg.Any<object>(), Arg.Any<CommandFlags>());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void VerifyRedisNotCalled()
|
||||||
|
{
|
||||||
|
_mockDb
|
||||||
|
.DidNotReceive()
|
||||||
|
.ScriptEvaluateAsync(Arg.Any<LuaScript>(), Arg.Any<object>(), Arg.Any<CommandFlags>());
|
||||||
}
|
}
|
||||||
|
|
||||||
private CustomRedisProcessingStrategy BuildProcessingStrategy(
|
private CustomRedisProcessingStrategy BuildProcessingStrategy(
|
||||||
@ -163,36 +171,33 @@ public class CustomRedisProcessingStrategyTests
|
|||||||
bool throwRedisTimeout = false,
|
bool throwRedisTimeout = false,
|
||||||
IMemoryCache mockCache = null)
|
IMemoryCache mockCache = null)
|
||||||
{
|
{
|
||||||
var mockRedisConnection = new Mock<IConnectionMultiplexer>();
|
var mockRedisConnection = Substitute.For<IConnectionMultiplexer>();
|
||||||
|
|
||||||
mockRedisConnection.Setup(x => x.IsConnected).Returns(isRedisConnected);
|
mockRedisConnection.IsConnected.Returns(isRedisConnected);
|
||||||
|
|
||||||
_mockDb = new Mock<IDatabase>();
|
_mockDb = Substitute.For<IDatabase>();
|
||||||
|
|
||||||
var mockScriptEvaluate = _mockDb
|
var mockScriptEvaluate = _mockDb
|
||||||
.Setup(x =>
|
.ScriptEvaluateAsync(Arg.Any<LuaScript>(), Arg.Any<object>(), Arg.Any<CommandFlags>());
|
||||||
x.ScriptEvaluateAsync(It.IsAny<LuaScript>(), It.IsAny<object>(), It.IsAny<CommandFlags>()));
|
|
||||||
|
|
||||||
if (throwRedisTimeout)
|
if (throwRedisTimeout)
|
||||||
{
|
{
|
||||||
mockScriptEvaluate.ThrowsAsync(new RedisTimeoutException("Timeout", CommandStatus.WaitingToBeSent));
|
mockScriptEvaluate.Returns<RedisResult>(x => throw new RedisTimeoutException("Timeout", CommandStatus.WaitingToBeSent));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
mockScriptEvaluate.ReturnsAsync(RedisResult.Create(1));
|
mockScriptEvaluate.Returns(RedisResult.Create(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
mockRedisConnection
|
mockRedisConnection.GetDatabase(Arg.Any<int>(), Arg.Any<object>())
|
||||||
.Setup(x =>
|
.Returns(_mockDb);
|
||||||
x.GetDatabase(It.IsAny<int>(), It.IsAny<object>()))
|
|
||||||
.Returns(_mockDb.Object);
|
|
||||||
|
|
||||||
var mockLogger = new Mock<ILogger<CustomRedisProcessingStrategy>>();
|
var mockLogger = Substitute.For<ILogger<CustomRedisProcessingStrategy>>();
|
||||||
var mockConfig = new Mock<IRateLimitConfiguration>();
|
var mockConfig = Substitute.For<IRateLimitConfiguration>();
|
||||||
|
|
||||||
mockCache ??= new Mock<IMemoryCache>().Object;
|
mockCache ??= Substitute.For<IMemoryCache>();
|
||||||
|
|
||||||
return new CustomRedisProcessingStrategy(mockRedisConnection.Object, mockConfig.Object,
|
return new CustomRedisProcessingStrategy(mockRedisConnection, mockConfig,
|
||||||
mockLogger.Object, mockCache, _sampleSettings);
|
mockLogger, mockCache, _sampleSettings);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -48,16 +48,6 @@
|
|||||||
"Microsoft.TestPlatform.TestHost": "17.1.0"
|
"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": {
|
"NSubstitute": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[4.3.0, )",
|
"requested": "[4.3.0, )",
|
||||||
@ -2680,55 +2670,55 @@
|
|||||||
"common": {
|
"common": {
|
||||||
"type": "Project",
|
"type": "Project",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"AutoFixture.AutoNSubstitute": "4.17.0",
|
"AutoFixture.AutoNSubstitute": "[4.17.0, )",
|
||||||
"AutoFixture.Xunit2": "4.17.0",
|
"AutoFixture.Xunit2": "[4.17.0, )",
|
||||||
"Core": "2023.7.2",
|
"Core": "[2023.7.2, )",
|
||||||
"Kralizek.AutoFixture.Extensions.MockHttp": "1.2.0",
|
"Kralizek.AutoFixture.Extensions.MockHttp": "[1.2.0, )",
|
||||||
"Microsoft.NET.Test.Sdk": "17.1.0",
|
"Microsoft.NET.Test.Sdk": "[17.1.0, )",
|
||||||
"NSubstitute": "4.3.0",
|
"NSubstitute": "[4.3.0, )",
|
||||||
"xunit": "2.4.1"
|
"xunit": "[2.4.1, )"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"core": {
|
"core": {
|
||||||
"type": "Project",
|
"type": "Project",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"AWSSDK.SQS": "3.7.2.47",
|
"AWSSDK.SQS": "[3.7.2.47, )",
|
||||||
"AWSSDK.SimpleEmail": "3.7.0.150",
|
"AWSSDK.SimpleEmail": "[3.7.0.150, )",
|
||||||
"AspNetCoreRateLimit": "4.0.2",
|
"AspNetCoreRateLimit": "[4.0.2, )",
|
||||||
"AspNetCoreRateLimit.Redis": "1.0.1",
|
"AspNetCoreRateLimit.Redis": "[1.0.1, )",
|
||||||
"Azure.Extensions.AspNetCore.DataProtection.Blobs": "1.3.2",
|
"Azure.Extensions.AspNetCore.DataProtection.Blobs": "[1.3.2, )",
|
||||||
"Azure.Messaging.ServiceBus": "7.15.0",
|
"Azure.Messaging.ServiceBus": "[7.15.0, )",
|
||||||
"Azure.Storage.Blobs": "12.14.1",
|
"Azure.Storage.Blobs": "[12.14.1, )",
|
||||||
"Azure.Storage.Queues": "12.12.0",
|
"Azure.Storage.Queues": "[12.12.0, )",
|
||||||
"BitPay.Light": "1.0.1907",
|
"BitPay.Light": "[1.0.1907, )",
|
||||||
"Braintree": "5.12.0",
|
"Braintree": "[5.12.0, )",
|
||||||
"DnsClient": "1.7.0",
|
"DnsClient": "[1.7.0, )",
|
||||||
"Fido2.AspNet": "3.0.1",
|
"Fido2.AspNet": "[3.0.1, )",
|
||||||
"Handlebars.Net": "2.1.2",
|
"Handlebars.Net": "[2.1.2, )",
|
||||||
"IdentityServer4": "4.1.2",
|
"IdentityServer4": "[4.1.2, )",
|
||||||
"IdentityServer4.AccessTokenValidation": "3.0.1",
|
"IdentityServer4.AccessTokenValidation": "[3.0.1, )",
|
||||||
"LaunchDarkly.ServerSdk": "7.0.0",
|
"LaunchDarkly.ServerSdk": "[7.0.0, )",
|
||||||
"MailKit": "3.2.0",
|
"MailKit": "[3.2.0, )",
|
||||||
"Microsoft.AspNetCore.Authentication.JwtBearer": "6.0.4",
|
"Microsoft.AspNetCore.Authentication.JwtBearer": "[6.0.4, )",
|
||||||
"Microsoft.Azure.Cosmos.Table": "1.0.8",
|
"Microsoft.Azure.Cosmos.Table": "[1.0.8, )",
|
||||||
"Microsoft.Azure.NotificationHubs": "4.1.0",
|
"Microsoft.Azure.NotificationHubs": "[4.1.0, )",
|
||||||
"Microsoft.Data.SqlClient": "5.0.1",
|
"Microsoft.Data.SqlClient": "[5.0.1, )",
|
||||||
"Microsoft.Extensions.Caching.StackExchangeRedis": "6.0.6",
|
"Microsoft.Extensions.Caching.StackExchangeRedis": "[6.0.6, )",
|
||||||
"Microsoft.Extensions.Configuration.EnvironmentVariables": "6.0.1",
|
"Microsoft.Extensions.Configuration.EnvironmentVariables": "[6.0.1, )",
|
||||||
"Microsoft.Extensions.Configuration.UserSecrets": "6.0.1",
|
"Microsoft.Extensions.Configuration.UserSecrets": "[6.0.1, )",
|
||||||
"Microsoft.Extensions.Identity.Stores": "6.0.4",
|
"Microsoft.Extensions.Identity.Stores": "[6.0.4, )",
|
||||||
"Newtonsoft.Json": "13.0.1",
|
"Newtonsoft.Json": "[13.0.1, )",
|
||||||
"Otp.NET": "1.2.2",
|
"Otp.NET": "[1.2.2, )",
|
||||||
"Quartz": "3.4.0",
|
"Quartz": "[3.4.0, )",
|
||||||
"SendGrid": "9.27.0",
|
"SendGrid": "[9.27.0, )",
|
||||||
"Sentry.Serilog": "3.16.0",
|
"Sentry.Serilog": "[3.16.0, )",
|
||||||
"Serilog.AspNetCore": "5.0.0",
|
"Serilog.AspNetCore": "[5.0.0, )",
|
||||||
"Serilog.Extensions.Logging": "3.1.0",
|
"Serilog.Extensions.Logging": "[3.1.0, )",
|
||||||
"Serilog.Extensions.Logging.File": "2.0.0",
|
"Serilog.Extensions.Logging.File": "[2.0.0, )",
|
||||||
"Serilog.Sinks.AzureCosmosDB": "2.0.0",
|
"Serilog.Sinks.AzureCosmosDB": "[2.0.0, )",
|
||||||
"Serilog.Sinks.SyslogMessages": "2.0.6",
|
"Serilog.Sinks.SyslogMessages": "[2.0.6, )",
|
||||||
"Stripe.net": "40.0.0",
|
"Stripe.net": "[40.0.0, )",
|
||||||
"YubicoDotNetClient": "1.2.0"
|
"YubicoDotNetClient": "[1.2.0, )"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\src\Icons\Icons.csproj" />
|
<ProjectReference Include="..\..\src\Icons\Icons.csproj" />
|
||||||
|
<ProjectReference Include="..\Common\Common.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
38
test/Icons.Test/Models/IconHttpRequestTests.cs
Normal file
38
test/Icons.Test/Models/IconHttpRequestTests.cs
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
using System.Net;
|
||||||
|
using Bit.Icons.Models;
|
||||||
|
using Bit.Icons.Services;
|
||||||
|
using Bit.Test.Common.MockedHttpClient;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using Microsoft.Net.Http.Headers;
|
||||||
|
using NSubstitute;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Icons.Test.Models;
|
||||||
|
|
||||||
|
public class IconHttpRequestTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task FetchAsync_FollowsTwoRedirectsAsync()
|
||||||
|
{
|
||||||
|
var handler = new MockedHttpMessageHandler();
|
||||||
|
|
||||||
|
var request = handler
|
||||||
|
.Fallback
|
||||||
|
.WithStatusCode(HttpStatusCode.Redirect)
|
||||||
|
.WithContent("text/html", "<html><head><title>Redirect 2</title></head><body><a href=\"https://icon.test\">Redirect 3</a></body></html>")
|
||||||
|
.WithHeader(HeaderNames.Location, "https://icon.test");
|
||||||
|
|
||||||
|
var clientFactory = Substitute.For<IHttpClientFactory>();
|
||||||
|
clientFactory.CreateClient("Icons").Returns(handler.ToHttpClient());
|
||||||
|
|
||||||
|
var uriService = Substitute.For<IUriService>();
|
||||||
|
uriService.TryGetUri(Arg.Any<Uri>(), out Arg.Any<IconUri>()).Returns(x =>
|
||||||
|
{
|
||||||
|
x[1] = new IconUri(new Uri("https://icon.test"), IPAddress.Parse("192.0.2.1"));
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
var result = await IconHttpRequest.FetchAsync(new Uri("https://icon.test"), NullLogger<IIconFetchingService>.Instance, clientFactory, uriService);
|
||||||
|
|
||||||
|
Assert.Equal(3, request.NumberOfResponses); // Initial + 2 redirects
|
||||||
|
}
|
||||||
|
}
|
101
test/Icons.Test/Models/IconHttpResponseTests.cs
Normal file
101
test/Icons.Test/Models/IconHttpResponseTests.cs
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
using System.Net;
|
||||||
|
using AngleSharp.Html.Parser;
|
||||||
|
using Bit.Icons.Models;
|
||||||
|
using Bit.Icons.Services;
|
||||||
|
using Bit.Test.Common.Helpers;
|
||||||
|
using Bit.Test.Common.MockedHttpClient;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using NSubstitute;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Icons.Test.Models;
|
||||||
|
|
||||||
|
public class IconHttpResponseTests
|
||||||
|
{
|
||||||
|
private readonly IUriService _mockedUriService;
|
||||||
|
private static readonly IHtmlParser _parser = new HtmlParser();
|
||||||
|
|
||||||
|
public IconHttpResponseTests()
|
||||||
|
{
|
||||||
|
_mockedUriService = Substitute.For<IUriService>();
|
||||||
|
_mockedUriService.TryGetUri(Arg.Any<Uri>(), out Arg.Any<IconUri>()).Returns(x =>
|
||||||
|
{
|
||||||
|
x[1] = new IconUri(new Uri("https://icon.test"), IPAddress.Parse("192.0.2.1"));
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RetrieveIconsAsync_Processes200LinksAsync()
|
||||||
|
{
|
||||||
|
var htmlBuilder = new HtmlBuilder();
|
||||||
|
var headBuilder = new HtmlBuilder("head");
|
||||||
|
for (var i = 0; i < 200; i++)
|
||||||
|
{
|
||||||
|
headBuilder.Append(UnusableLinkNode());
|
||||||
|
}
|
||||||
|
headBuilder.Append(UsableLinkNode());
|
||||||
|
htmlBuilder.Append(headBuilder);
|
||||||
|
var response = GetHttpResponseMessage(htmlBuilder.ToString());
|
||||||
|
var sut = CurriedIconHttpResponse()(response);
|
||||||
|
|
||||||
|
var result = await sut.RetrieveIconsAsync(new Uri("https://icon.test"), "icon.test", _parser);
|
||||||
|
|
||||||
|
Assert.Empty(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RetrieveIconsAsync_Processes10IconsAsync()
|
||||||
|
{
|
||||||
|
var htmlBuilder = new HtmlBuilder();
|
||||||
|
var headBuilder = new HtmlBuilder("head");
|
||||||
|
for (var i = 0; i < 11; i++)
|
||||||
|
{
|
||||||
|
headBuilder.Append(UsableLinkNode());
|
||||||
|
}
|
||||||
|
htmlBuilder.Append(headBuilder);
|
||||||
|
var response = GetHttpResponseMessage(htmlBuilder.ToString());
|
||||||
|
var sut = CurriedIconHttpResponse()(response);
|
||||||
|
|
||||||
|
var result = await sut.RetrieveIconsAsync(new Uri("https://icon.test"), "icon.test", _parser);
|
||||||
|
|
||||||
|
Assert.Equal(10, result.Count());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string UsableLinkNode()
|
||||||
|
{
|
||||||
|
return "<link rel=\"icon\" href=\"https://icon.test/favicon.ico\" />";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string UnusableLinkNode()
|
||||||
|
{
|
||||||
|
// Empty href links are not usable
|
||||||
|
return "<link rel=\"icon\" href=\"\" />";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HttpResponseMessage GetHttpResponseMessage(string content)
|
||||||
|
{
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
RequestMessage = new HttpRequestMessage(HttpMethod.Get, "https://icon.test"),
|
||||||
|
Content = new StringContent(content)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private Func<HttpResponseMessage, IconHttpResponse> CurriedIconHttpResponse()
|
||||||
|
{
|
||||||
|
return (HttpResponseMessage response) => new IconHttpResponse(response, NullLogger<IIconFetchingService>.Instance, UsableIconHttpClientFactory(), _mockedUriService);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IHttpClientFactory UsableIconHttpClientFactory()
|
||||||
|
{
|
||||||
|
var substitute = Substitute.For<IHttpClientFactory>();
|
||||||
|
var handler = new MockedHttpMessageHandler();
|
||||||
|
handler.Fallback
|
||||||
|
.WithStatusCode(HttpStatusCode.OK)
|
||||||
|
.WithContent("image/png", new byte[] { 137, 80, 78, 71 });
|
||||||
|
|
||||||
|
substitute.CreateClient("Icons").Returns(handler.ToHttpClient());
|
||||||
|
return substitute;
|
||||||
|
}
|
||||||
|
}
|
85
test/Icons.Test/Models/IconLinkTests.cs
Normal file
85
test/Icons.Test/Models/IconLinkTests.cs
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
using System.Net;
|
||||||
|
using AngleSharp.Dom;
|
||||||
|
using Bit.Icons.Models;
|
||||||
|
using Bit.Icons.Services;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using NSubstitute;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Icons.Test.Models;
|
||||||
|
|
||||||
|
public class IconLinkTests
|
||||||
|
{
|
||||||
|
private readonly IElement _element;
|
||||||
|
private readonly Uri _uri = new("https://icon.test");
|
||||||
|
private readonly ILogger<IIconFetchingService> _logger = Substitute.For<ILogger<IIconFetchingService>>();
|
||||||
|
private readonly IHttpClientFactory _httpClientFactory;
|
||||||
|
private readonly IUriService _uriService;
|
||||||
|
private readonly string _baseUrlPath = "/";
|
||||||
|
|
||||||
|
public IconLinkTests()
|
||||||
|
{
|
||||||
|
_element = Substitute.For<IElement>();
|
||||||
|
_httpClientFactory = Substitute.For<IHttpClientFactory>();
|
||||||
|
_uriService = Substitute.For<IUriService>();
|
||||||
|
_uriService.TryGetUri(Arg.Any<Uri>(), out Arg.Any<IconUri>()).Returns(x =>
|
||||||
|
{
|
||||||
|
x[1] = new IconUri(new Uri("https://icon.test"), IPAddress.Parse("192.0.2.1"));
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WithNoHref_IsNotUsable()
|
||||||
|
{
|
||||||
|
_element.GetAttribute("href").Returns(string.Empty);
|
||||||
|
|
||||||
|
var result = new IconLink(_element, _uri, _baseUrlPath).IsUsable();
|
||||||
|
|
||||||
|
Assert.False(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(null, false)]
|
||||||
|
[InlineData("", false)]
|
||||||
|
[InlineData(" ", false)]
|
||||||
|
[InlineData("unusable", false)]
|
||||||
|
[InlineData("ico", true)]
|
||||||
|
public void WithNoRel_IsUsable(string extension, bool expectedResult)
|
||||||
|
{
|
||||||
|
SetAttributeValue("href", $"/favicon.{extension}");
|
||||||
|
|
||||||
|
var result = new IconLink(_element, _uri, _baseUrlPath).IsUsable();
|
||||||
|
|
||||||
|
Assert.Equal(expectedResult, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("icon", true)]
|
||||||
|
[InlineData("stylesheet", false)]
|
||||||
|
public void WithRel_IsUsable(string rel, bool expectedResult)
|
||||||
|
{
|
||||||
|
SetAttributeValue("href", "/favicon.ico");
|
||||||
|
SetAttributeValue("rel", rel);
|
||||||
|
|
||||||
|
var result = new IconLink(_element, _uri, _baseUrlPath).IsUsable();
|
||||||
|
|
||||||
|
Assert.Equal(expectedResult, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FetchAsync_Unvalidated_ReturnsNull()
|
||||||
|
{
|
||||||
|
var result = new IconLink(_element, _uri, _baseUrlPath).FetchAsync(_logger, _httpClientFactory, _uriService);
|
||||||
|
|
||||||
|
Assert.Null(result.Result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetAttributeValue(string attribute, string value)
|
||||||
|
{
|
||||||
|
var attr = Substitute.For<IAttr>();
|
||||||
|
attr.Value.Returns(value);
|
||||||
|
|
||||||
|
_element.Attributes[attribute].Returns(attr);
|
||||||
|
}
|
||||||
|
}
|
22
test/Icons.Test/Models/IconUriTests.cs
Normal file
22
test/Icons.Test/Models/IconUriTests.cs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
using System.Net;
|
||||||
|
using Bit.Icons.Models;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Icons.Test.Models;
|
||||||
|
|
||||||
|
public class IconUriTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData("https://icon.test", "1.1.1.1", true)]
|
||||||
|
[InlineData("https://icon.test:4443", "1.1.1.1", false)] // Non standard port
|
||||||
|
[InlineData("http://test", "1.1.1.1", false)] // top level domain
|
||||||
|
[InlineData("https://icon.test", "127.0.0.1", false)] // IP is internal
|
||||||
|
[InlineData("https://icon.test", "::1", false)] // IP is internal
|
||||||
|
[InlineData("https://1.1.1.1", "::1", false)] // host is IP
|
||||||
|
public void IsValid(string uri, string ip, bool expectedResult)
|
||||||
|
{
|
||||||
|
var result = new IconUri(new Uri(uri), IPAddress.Parse(ip)).IsValid;
|
||||||
|
|
||||||
|
Assert.Equal(expectedResult, result);
|
||||||
|
}
|
||||||
|
}
|
@ -1,25 +1,25 @@
|
|||||||
using Bit.Icons.Services;
|
using Bit.Icons.Services;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Bit.Icons.Test.Services;
|
namespace Bit.Icons.Test.Services;
|
||||||
|
|
||||||
public class IconFetchingServiceTests
|
public class IconFetchingServiceTests : ServiceTestBase<IconFetchingService>
|
||||||
{
|
{
|
||||||
[Theory]
|
[Theory]
|
||||||
|
[InlineData("www.twitter.com")] // https site
|
||||||
[InlineData("www.google.com")] // https site
|
[InlineData("www.google.com")] // https site
|
||||||
[InlineData("neverssl.com")] // http site
|
[InlineData("neverssl.com")] // http site
|
||||||
[InlineData("ameritrade.com")]
|
[InlineData("neopets.com")] // uses favicon.ico
|
||||||
|
[InlineData("hopin.com")] // uses svg+xml format
|
||||||
|
[InlineData("ameritrade.com")] // redirects to tdameritrace.com
|
||||||
[InlineData("icloud.com")]
|
[InlineData("icloud.com")]
|
||||||
[InlineData("bofa.com", Skip = "Broken in pipeline for .NET 6. Tracking link: https://bitwarden.atlassian.net/browse/PS-982")]
|
[InlineData("bofa.com", Skip = "Broken in pipeline for .NET 6. Tracking link: https://bitwarden.atlassian.net/browse/PS-982")]
|
||||||
public async Task GetIconAsync_Success(string domain)
|
public async Task GetIconAsync_Success(string domain)
|
||||||
{
|
{
|
||||||
var sut = new IconFetchingService(GetLogger());
|
var sut = BuildSut();
|
||||||
var result = await sut.GetIconAsync(domain);
|
var result = await sut.GetIconAsync(domain);
|
||||||
|
|
||||||
Assert.NotNull(result);
|
Assert.NotNull(result);
|
||||||
Assert.NotNull(result.Icon);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
@ -28,23 +28,12 @@ public class IconFetchingServiceTests
|
|||||||
[InlineData("localhost")]
|
[InlineData("localhost")]
|
||||||
public async Task GetIconAsync_ReturnsNull(string domain)
|
public async Task GetIconAsync_ReturnsNull(string domain)
|
||||||
{
|
{
|
||||||
var sut = new IconFetchingService(GetLogger());
|
var sut = BuildSut();
|
||||||
var result = await sut.GetIconAsync(domain);
|
var result = await sut.GetIconAsync(domain);
|
||||||
|
|
||||||
Assert.Null(result);
|
Assert.Null(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ILogger<IconFetchingService> GetLogger()
|
private IconFetchingService BuildSut() =>
|
||||||
{
|
GetService<IconFetchingService>();
|
||||||
var services = new ServiceCollection();
|
|
||||||
services.AddLogging(b =>
|
|
||||||
{
|
|
||||||
b.ClearProviders();
|
|
||||||
b.AddDebug();
|
|
||||||
});
|
|
||||||
|
|
||||||
var provider = services.BuildServiceProvider();
|
|
||||||
|
|
||||||
return provider.GetRequiredService<ILogger<IconFetchingService>>();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
41
test/Icons.Test/Services/ServiceTestBase.cs
Normal file
41
test/Icons.Test/Services/ServiceTestBase.cs
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
using Bit.Icons.Extensions;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Bit.Icons.Test.Services;
|
||||||
|
|
||||||
|
public class ServiceTestBase
|
||||||
|
{
|
||||||
|
internal ServiceCollection _services = new();
|
||||||
|
internal ServiceProvider _provider;
|
||||||
|
|
||||||
|
public ServiceTestBase()
|
||||||
|
{
|
||||||
|
_services = new ServiceCollection();
|
||||||
|
_services.AddLogging(b =>
|
||||||
|
{
|
||||||
|
b.ClearProviders();
|
||||||
|
b.AddDebug();
|
||||||
|
});
|
||||||
|
|
||||||
|
_services.ConfigureHttpClients();
|
||||||
|
_services.AddHtmlParsing();
|
||||||
|
_services.AddServices();
|
||||||
|
|
||||||
|
_provider = _services.BuildServiceProvider();
|
||||||
|
}
|
||||||
|
|
||||||
|
public T GetService<T>() =>
|
||||||
|
_provider.GetRequiredService<T>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ServiceTestBase<TSut> : ServiceTestBase where TSut : class
|
||||||
|
{
|
||||||
|
public ServiceTestBase() : base()
|
||||||
|
{
|
||||||
|
_services.AddTransient<TSut>();
|
||||||
|
_provider = _services.BuildServiceProvider();
|
||||||
|
}
|
||||||
|
|
||||||
|
public TSut Sut => GetService<TSut>();
|
||||||
|
}
|
@ -46,11 +46,10 @@
|
|||||||
},
|
},
|
||||||
"AngleSharp": {
|
"AngleSharp": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "0.16.1",
|
"resolved": "1.0.4",
|
||||||
"contentHash": "1k7Vbfmr5IUsGaR0QJwTe8XF9zacFUIoWxMgI4X/ipiyKxCWZJZoaG96fNEugL90iubvboRvE1IxuBPibET/Rg==",
|
"contentHash": "G8R4C2MEDFQvjUbYz1QIcGfibjsTJnzP0aWy8iQgWWk7eKacYydCNGD3JMhVL0Q5pZQ9RYlqpKNESEU5NpqsRw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"System.Buffers": "4.5.1",
|
"System.Text.Encoding.CodePages": "6.0.0"
|
||||||
"System.Text.Encoding.CodePages": "5.0.0"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"AspNetCoreRateLimit": {
|
"AspNetCoreRateLimit": {
|
||||||
@ -73,6 +72,33 @@
|
|||||||
"StackExchange.Redis": "2.5.43"
|
"StackExchange.Redis": "2.5.43"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"AutoFixture": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "4.17.0",
|
||||||
|
"contentHash": "efMRCG3Epc4QDELwdmQGf6/caQUleRXPRCnLAq5gLMpTuOTcOQWV12vEJ8qo678Rj97/TjjxHYu/34rGkXdVAA==",
|
||||||
|
"dependencies": {
|
||||||
|
"Fare": "[2.1.1, 3.0.0)",
|
||||||
|
"System.ComponentModel.Annotations": "4.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AutoFixture.AutoNSubstitute": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "4.17.0",
|
||||||
|
"contentHash": "iWsRiDQ7T8s6F4mvYbSvPTq0GDtxJD6D+E1Fu9gVbHUvJiNikC1yIDNTH+3tQF7RK864HH/3R8ETj9m2X8UXvg==",
|
||||||
|
"dependencies": {
|
||||||
|
"AutoFixture": "4.17.0",
|
||||||
|
"NSubstitute": "[2.0.3, 5.0.0)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AutoFixture.Xunit2": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "4.17.0",
|
||||||
|
"contentHash": "lrURL/LhJLPkn2tSPUEW8Wscr5LoV2Mr8A+ikn5gwkofex3o7qWUsBswlLw+KCA7EOpeqwZOldp3k91zDF+48Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"AutoFixture": "4.17.0",
|
||||||
|
"xunit.extensibility.core": "[2.2.0, 3.0.0)"
|
||||||
|
}
|
||||||
|
},
|
||||||
"AutoMapper": {
|
"AutoMapper": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "12.0.1",
|
"resolved": "12.0.1",
|
||||||
@ -246,6 +272,14 @@
|
|||||||
"Microsoft.Win32.Registry": "5.0.0"
|
"Microsoft.Win32.Registry": "5.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Fare": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.1.1",
|
||||||
|
"contentHash": "HaI8puqA66YU7/9cK4Sgbs1taUTP1Ssa4QT2PIzqJ7GvAbN1QgkjbRsjH+FSbMh1MJdvS0CIwQNLtFT+KF6KpA==",
|
||||||
|
"dependencies": {
|
||||||
|
"NETStandard.Library": "1.6.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"Fido2": {
|
"Fido2": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.0.1",
|
"resolved": "3.0.1",
|
||||||
@ -326,6 +360,15 @@
|
|||||||
"IdentityModel": "4.4.0"
|
"IdentityModel": "4.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Kralizek.AutoFixture.Extensions.MockHttp": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "1.2.0",
|
||||||
|
"contentHash": "6zmks7/5mVczazv910N7V2EdiU6B+rY61lwdgVO0o2iZuTI6KI3T+Hgkrjv0eGOKYucq2OMC+gnAc5Ej2ajoTQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"AutoFixture": "4.11.0",
|
||||||
|
"RichardSzalay.MockHttp": "6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"LaunchDarkly.Cache": {
|
"LaunchDarkly.Cache": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "1.0.2",
|
"resolved": "1.0.2",
|
||||||
@ -1148,6 +1191,11 @@
|
|||||||
"System.Diagnostics.DiagnosticSource": "4.7.1"
|
"System.Diagnostics.DiagnosticSource": "4.7.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"RichardSzalay.MockHttp": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "6.0.0",
|
||||||
|
"contentHash": "bStGNqIX/MGYtML7K3EzdsE/k5HGVAcg7XgN23TQXGXqxNC9fvYFR94fA0sGM5hAT36R+BBGet6ZDQxXL/IPxg=="
|
||||||
|
},
|
||||||
"runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": {
|
"runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "4.3.2",
|
"resolved": "4.3.2",
|
||||||
@ -1568,6 +1616,24 @@
|
|||||||
"System.Runtime": "4.3.0"
|
"System.Runtime": "4.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"System.ComponentModel.Annotations": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "4.3.0",
|
||||||
|
"contentHash": "SY2RLItHt43rd8J9D8M8e8NM4m+9WLN2uUd9G0n1I4hj/7w+v3pzK6ZBjexlG1/2xvLKQsqir3UGVSyBTXMLWA==",
|
||||||
|
"dependencies": {
|
||||||
|
"System.Collections": "4.3.0",
|
||||||
|
"System.ComponentModel": "4.3.0",
|
||||||
|
"System.Globalization": "4.3.0",
|
||||||
|
"System.Linq": "4.3.0",
|
||||||
|
"System.Reflection": "4.3.0",
|
||||||
|
"System.Reflection.Extensions": "4.3.0",
|
||||||
|
"System.Resources.ResourceManager": "4.3.0",
|
||||||
|
"System.Runtime": "4.3.0",
|
||||||
|
"System.Runtime.Extensions": "4.3.0",
|
||||||
|
"System.Text.RegularExpressions": "4.3.0",
|
||||||
|
"System.Threading": "4.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"System.ComponentModel.Primitives": {
|
"System.ComponentModel.Primitives": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "4.3.0",
|
"resolved": "4.3.0",
|
||||||
@ -2509,10 +2575,10 @@
|
|||||||
},
|
},
|
||||||
"System.Text.Encoding.CodePages": {
|
"System.Text.Encoding.CodePages": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "5.0.0",
|
"resolved": "6.0.0",
|
||||||
"contentHash": "NyscU59xX6Uo91qvhOs2Ccho3AR2TnZPomo1Z0K6YpyztBPM/A5VbkzOO19sy3A3i1TtEnTxA7bCe3Us+r5MWg==",
|
"contentHash": "ZFCILZuOvtKPauZ/j/swhvw68ZRi9ATCfvGbk1QfydmcXBkIWecWKn/250UH7rahZ5OoDBaiAudJtPvLwzw85A==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.NETCore.Platforms": "5.0.0"
|
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"System.Text.Encoding.Extensions": {
|
"System.Text.Encoding.Extensions": {
|
||||||
@ -2770,6 +2836,18 @@
|
|||||||
"NETStandard.Library": "1.6.1"
|
"NETStandard.Library": "1.6.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"common": {
|
||||||
|
"type": "Project",
|
||||||
|
"dependencies": {
|
||||||
|
"AutoFixture.AutoNSubstitute": "[4.17.0, )",
|
||||||
|
"AutoFixture.Xunit2": "[4.17.0, )",
|
||||||
|
"Core": "[2023.7.2, )",
|
||||||
|
"Kralizek.AutoFixture.Extensions.MockHttp": "[1.2.0, )",
|
||||||
|
"Microsoft.NET.Test.Sdk": "[17.1.0, )",
|
||||||
|
"NSubstitute": "[4.3.0, )",
|
||||||
|
"xunit": "[2.4.1, )"
|
||||||
|
}
|
||||||
|
},
|
||||||
"core": {
|
"core": {
|
||||||
"type": "Project",
|
"type": "Project",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -2815,7 +2893,7 @@
|
|||||||
"icons": {
|
"icons": {
|
||||||
"type": "Project",
|
"type": "Project",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"AngleSharp": "0.16.1",
|
"AngleSharp": "1.0.4",
|
||||||
"Core": "2023.7.2",
|
"Core": "2023.7.2",
|
||||||
"SharedWeb": "2023.7.2"
|
"SharedWeb": "2023.7.2"
|
||||||
}
|
}
|
||||||
|
@ -33,7 +33,7 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
|
|||||||
using var body = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
|
using var body = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
|
||||||
var endpointRoot = body.RootElement;
|
var endpointRoot = body.RootElement;
|
||||||
|
|
||||||
// WARNING: Edits to this file should NOT just be made to "get the test to work" they should be made when intentional
|
// WARNING: Edits to this file should NOT just be made to "get the test to work" they should be made when intentional
|
||||||
// changes were made to this endpoint and proper testing will take place to ensure clients are backwards compatible
|
// changes were made to this endpoint and proper testing will take place to ensure clients are backwards compatible
|
||||||
// or loss of functionality is properly noted.
|
// or loss of functionality is properly noted.
|
||||||
await using var fs = File.OpenRead("openid-configuration.json");
|
await using var fs = File.OpenRead("openid-configuration.json");
|
||||||
@ -372,10 +372,10 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// This test currently does not test any code that is not covered by other tests but
|
/// This test currently does not test any code that is not covered by other tests but
|
||||||
/// it shows that we probably have some dead code in <see cref="ClientStore"/>
|
/// it shows that we probably have some dead code in <see cref="ClientStore"/>
|
||||||
/// for installation, organization, and user they split on a <c>'.'</c> but have already checked that at least one
|
/// for installation, organization, and user they split on a <c>'.'</c> but have already checked that at least one
|
||||||
/// <c>'.'</c> exists in the <c>client_id</c> by checking it with <see cref="string.StartsWith(string)"/>
|
/// <c>'.'</c> exists in the <c>client_id</c> by checking it with <see cref="string.StartsWith(string)"/>
|
||||||
/// I believe that idParts.Length > 1 will ALWAYS return true
|
/// I believe that idParts.Length > 1 will ALWAYS return true
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -488,9 +488,9 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task TokenEndpoint_ToQuickInOneSecond_BlockRequest(string deviceId)
|
public async Task TokenEndpoint_TooQuickInOneSecond_BlockRequest(string deviceId)
|
||||||
{
|
{
|
||||||
const int AmountInOneSecondAllowed = 5;
|
const int AmountInOneSecondAllowed = 10;
|
||||||
|
|
||||||
// The rule we are testing is 10 requests in 1 second
|
// The rule we are testing is 10 requests in 1 second
|
||||||
var username = "test+ratelimiting@email.com";
|
var username = "test+ratelimiting@email.com";
|
||||||
@ -514,9 +514,9 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
|
|||||||
}
|
}
|
||||||
|
|
||||||
var responses = (await Task.WhenAll(tasks)).ToList();
|
var responses = (await Task.WhenAll(tasks)).ToList();
|
||||||
|
var blockResponses = responses.Where(c => c.Response.StatusCode == StatusCodes.Status429TooManyRequests);
|
||||||
|
|
||||||
Assert.Equal(5, responses.Count(c => c.Response.StatusCode == StatusCodes.Status200OK));
|
Assert.True(blockResponses.Count() > 0);
|
||||||
Assert.Equal(1, responses.Count(c => c.Response.StatusCode == StatusCodes.Status429TooManyRequests));
|
|
||||||
|
|
||||||
Task<HttpContext> MakeRequest()
|
Task<HttpContext> MakeRequest()
|
||||||
{
|
{
|
||||||
|
@ -10,7 +10,7 @@ using Bit.Infrastructure.EntityFramework.Repositories;
|
|||||||
using Bit.Infrastructure.EntityFramework.Vault.Models;
|
using Bit.Infrastructure.EntityFramework.Vault.Models;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Moq;
|
using NSubstitute;
|
||||||
|
|
||||||
namespace Bit.Infrastructure.EFIntegration.Test.AutoFixture;
|
namespace Bit.Infrastructure.EFIntegration.Test.AutoFixture;
|
||||||
|
|
||||||
@ -25,20 +25,16 @@ internal class ServiceScopeFactoryBuilder : ISpecimenBuilder
|
|||||||
public object Create(object request, ISpecimenContext context)
|
public object Create(object request, ISpecimenContext context)
|
||||||
{
|
{
|
||||||
var fixture = new Fixture();
|
var fixture = new Fixture();
|
||||||
var serviceProvider = new Mock<IServiceProvider>();
|
var serviceProvider = Substitute.For<IServiceProvider>();
|
||||||
var dbContext = new DatabaseContext(_options);
|
var dbContext = new DatabaseContext(_options);
|
||||||
serviceProvider
|
serviceProvider.GetService(typeof(DatabaseContext)).Returns(dbContext);
|
||||||
.Setup(x => x.GetService(typeof(DatabaseContext)))
|
|
||||||
.Returns(dbContext);
|
|
||||||
|
|
||||||
var serviceScope = new Mock<IServiceScope>();
|
var serviceScope = Substitute.For<IServiceScope>();
|
||||||
serviceScope.Setup(x => x.ServiceProvider).Returns(serviceProvider.Object);
|
serviceScope.ServiceProvider.Returns(serviceProvider);
|
||||||
|
|
||||||
var serviceScopeFactory = new Mock<IServiceScopeFactory>();
|
var serviceScopeFactory = Substitute.For<IServiceScopeFactory>();
|
||||||
serviceScopeFactory
|
serviceScopeFactory.CreateScope().Returns(serviceScope);
|
||||||
.Setup(x => x.CreateScope())
|
return serviceScopeFactory;
|
||||||
.Returns(serviceScope.Object);
|
|
||||||
return serviceScopeFactory.Object;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ using Bit.Infrastructure.EntityFramework.Repositories;
|
|||||||
using Microsoft.AspNetCore.DataProtection;
|
using Microsoft.AspNetCore.DataProtection;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Moq;
|
using NSubstitute;
|
||||||
|
|
||||||
namespace Bit.Infrastructure.EFIntegration.Test.Helpers;
|
namespace Bit.Infrastructure.EFIntegration.Test.Helpers;
|
||||||
|
|
||||||
@ -18,17 +18,17 @@ public static class DatabaseOptionsFactory
|
|||||||
var services = new ServiceCollection()
|
var services = new ServiceCollection()
|
||||||
.AddSingleton(sp =>
|
.AddSingleton(sp =>
|
||||||
{
|
{
|
||||||
var dataProtector = new Mock<IDataProtector>();
|
var dataProtector = Substitute.For<IDataProtector>();
|
||||||
dataProtector
|
dataProtector.Unprotect(Arg.Any<byte[]>())
|
||||||
.Setup(d => d.Unprotect(It.IsAny<byte[]>()))
|
.Returns<byte[]>(data =>
|
||||||
.Returns<byte[]>(data => Encoding.UTF8.GetBytes(Constants.DatabaseFieldProtectedPrefix + Encoding.UTF8.GetString(data)));
|
Encoding.UTF8.GetBytes(Constants.DatabaseFieldProtectedPrefix +
|
||||||
|
Encoding.UTF8.GetString((byte[])data[0])));
|
||||||
|
|
||||||
var dataProtectionProvider = new Mock<IDataProtectionProvider>();
|
var dataProtectionProvider = Substitute.For<IDataProtectionProvider>();
|
||||||
dataProtectionProvider
|
dataProtectionProvider.CreateProtector(Constants.DatabaseFieldProtectorPurpose)
|
||||||
.Setup(x => x.CreateProtector(Constants.DatabaseFieldProtectorPurpose))
|
.Returns(dataProtector);
|
||||||
.Returns(dataProtector.Object);
|
|
||||||
|
|
||||||
return dataProtectionProvider.Object;
|
return dataProtectionProvider;
|
||||||
})
|
})
|
||||||
.BuildServiceProvider();
|
.BuildServiceProvider();
|
||||||
|
|
||||||
|
@ -8,7 +8,6 @@
|
|||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNetTestSdkVersion)" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNetTestSdkVersion)" />
|
||||||
<PackageReference Include="Moq" Version="4.17.2" />
|
|
||||||
<PackageReference Include="NSubstitute" Version="$(NSubstituteVersion)" />
|
<PackageReference Include="NSubstitute" Version="$(NSubstituteVersion)" />
|
||||||
<PackageReference Include="xunit" Version="$(XUnitVersion)" />
|
<PackageReference Include="xunit" Version="$(XUnitVersion)" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="$(XUnitRunnerVisualStudioVersion)">
|
<PackageReference Include="xunit.runner.visualstudio" Version="$(XUnitRunnerVisualStudioVersion)">
|
||||||
|
@ -38,16 +38,6 @@
|
|||||||
"Microsoft.TestPlatform.TestHost": "17.1.0"
|
"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": {
|
"NSubstitute": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[4.3.0, )",
|
"requested": "[4.3.0, )",
|
||||||
@ -2843,89 +2833,88 @@
|
|||||||
"common": {
|
"common": {
|
||||||
"type": "Project",
|
"type": "Project",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"AutoFixture.AutoNSubstitute": "4.17.0",
|
"AutoFixture.AutoNSubstitute": "[4.17.0, )",
|
||||||
"AutoFixture.Xunit2": "4.17.0",
|
"AutoFixture.Xunit2": "[4.17.0, )",
|
||||||
"Core": "2023.7.2",
|
"Core": "[2023.7.2, )",
|
||||||
"Kralizek.AutoFixture.Extensions.MockHttp": "1.2.0",
|
"Kralizek.AutoFixture.Extensions.MockHttp": "[1.2.0, )",
|
||||||
"Microsoft.NET.Test.Sdk": "17.1.0",
|
"Microsoft.NET.Test.Sdk": "[17.1.0, )",
|
||||||
"NSubstitute": "4.3.0",
|
"NSubstitute": "[4.3.0, )",
|
||||||
"xunit": "2.4.1"
|
"xunit": "[2.4.1, )"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"core": {
|
"core": {
|
||||||
"type": "Project",
|
"type": "Project",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"AWSSDK.SQS": "3.7.2.47",
|
"AWSSDK.SQS": "[3.7.2.47, )",
|
||||||
"AWSSDK.SimpleEmail": "3.7.0.150",
|
"AWSSDK.SimpleEmail": "[3.7.0.150, )",
|
||||||
"AspNetCoreRateLimit": "4.0.2",
|
"AspNetCoreRateLimit": "[4.0.2, )",
|
||||||
"AspNetCoreRateLimit.Redis": "1.0.1",
|
"AspNetCoreRateLimit.Redis": "[1.0.1, )",
|
||||||
"Azure.Extensions.AspNetCore.DataProtection.Blobs": "1.3.2",
|
"Azure.Extensions.AspNetCore.DataProtection.Blobs": "[1.3.2, )",
|
||||||
"Azure.Messaging.ServiceBus": "7.15.0",
|
"Azure.Messaging.ServiceBus": "[7.15.0, )",
|
||||||
"Azure.Storage.Blobs": "12.14.1",
|
"Azure.Storage.Blobs": "[12.14.1, )",
|
||||||
"Azure.Storage.Queues": "12.12.0",
|
"Azure.Storage.Queues": "[12.12.0, )",
|
||||||
"BitPay.Light": "1.0.1907",
|
"BitPay.Light": "[1.0.1907, )",
|
||||||
"Braintree": "5.12.0",
|
"Braintree": "[5.12.0, )",
|
||||||
"DnsClient": "1.7.0",
|
"DnsClient": "[1.7.0, )",
|
||||||
"Fido2.AspNet": "3.0.1",
|
"Fido2.AspNet": "[3.0.1, )",
|
||||||
"Handlebars.Net": "2.1.2",
|
"Handlebars.Net": "[2.1.2, )",
|
||||||
"IdentityServer4": "4.1.2",
|
"IdentityServer4": "[4.1.2, )",
|
||||||
"IdentityServer4.AccessTokenValidation": "3.0.1",
|
"IdentityServer4.AccessTokenValidation": "[3.0.1, )",
|
||||||
"LaunchDarkly.ServerSdk": "7.0.0",
|
"LaunchDarkly.ServerSdk": "[7.0.0, )",
|
||||||
"MailKit": "3.2.0",
|
"MailKit": "[3.2.0, )",
|
||||||
"Microsoft.AspNetCore.Authentication.JwtBearer": "6.0.4",
|
"Microsoft.AspNetCore.Authentication.JwtBearer": "[6.0.4, )",
|
||||||
"Microsoft.Azure.Cosmos.Table": "1.0.8",
|
"Microsoft.Azure.Cosmos.Table": "[1.0.8, )",
|
||||||
"Microsoft.Azure.NotificationHubs": "4.1.0",
|
"Microsoft.Azure.NotificationHubs": "[4.1.0, )",
|
||||||
"Microsoft.Data.SqlClient": "5.0.1",
|
"Microsoft.Data.SqlClient": "[5.0.1, )",
|
||||||
"Microsoft.Extensions.Caching.StackExchangeRedis": "6.0.6",
|
"Microsoft.Extensions.Caching.StackExchangeRedis": "[6.0.6, )",
|
||||||
"Microsoft.Extensions.Configuration.EnvironmentVariables": "6.0.1",
|
"Microsoft.Extensions.Configuration.EnvironmentVariables": "[6.0.1, )",
|
||||||
"Microsoft.Extensions.Configuration.UserSecrets": "6.0.1",
|
"Microsoft.Extensions.Configuration.UserSecrets": "[6.0.1, )",
|
||||||
"Microsoft.Extensions.Identity.Stores": "6.0.4",
|
"Microsoft.Extensions.Identity.Stores": "[6.0.4, )",
|
||||||
"Newtonsoft.Json": "13.0.1",
|
"Newtonsoft.Json": "[13.0.1, )",
|
||||||
"Otp.NET": "1.2.2",
|
"Otp.NET": "[1.2.2, )",
|
||||||
"Quartz": "3.4.0",
|
"Quartz": "[3.4.0, )",
|
||||||
"SendGrid": "9.27.0",
|
"SendGrid": "[9.27.0, )",
|
||||||
"Sentry.Serilog": "3.16.0",
|
"Sentry.Serilog": "[3.16.0, )",
|
||||||
"Serilog.AspNetCore": "5.0.0",
|
"Serilog.AspNetCore": "[5.0.0, )",
|
||||||
"Serilog.Extensions.Logging": "3.1.0",
|
"Serilog.Extensions.Logging": "[3.1.0, )",
|
||||||
"Serilog.Extensions.Logging.File": "2.0.0",
|
"Serilog.Extensions.Logging.File": "[2.0.0, )",
|
||||||
"Serilog.Sinks.AzureCosmosDB": "2.0.0",
|
"Serilog.Sinks.AzureCosmosDB": "[2.0.0, )",
|
||||||
"Serilog.Sinks.SyslogMessages": "2.0.6",
|
"Serilog.Sinks.SyslogMessages": "[2.0.6, )",
|
||||||
"Stripe.net": "40.0.0",
|
"Stripe.net": "[40.0.0, )",
|
||||||
"YubicoDotNetClient": "1.2.0"
|
"YubicoDotNetClient": "[1.2.0, )"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"core.test": {
|
"core.test": {
|
||||||
"type": "Project",
|
"type": "Project",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"AutoFixture.AutoNSubstitute": "4.17.0",
|
"AutoFixture.AutoNSubstitute": "[4.17.0, )",
|
||||||
"AutoFixture.Xunit2": "4.17.0",
|
"AutoFixture.Xunit2": "[4.17.0, )",
|
||||||
"Common": "2023.7.2",
|
"Common": "[2023.7.2, )",
|
||||||
"Core": "2023.7.2",
|
"Core": "[2023.7.2, )",
|
||||||
"Kralizek.AutoFixture.Extensions.MockHttp": "1.2.0",
|
"Kralizek.AutoFixture.Extensions.MockHttp": "[1.2.0, )",
|
||||||
"Microsoft.NET.Test.Sdk": "17.1.0",
|
"Microsoft.NET.Test.Sdk": "[17.1.0, )",
|
||||||
"Moq": "4.17.2",
|
"NSubstitute": "[4.3.0, )",
|
||||||
"NSubstitute": "4.3.0",
|
"xunit": "[2.4.1, )"
|
||||||
"xunit": "2.4.1"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"infrastructure.dapper": {
|
"infrastructure.dapper": {
|
||||||
"type": "Project",
|
"type": "Project",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Core": "2023.7.2",
|
"Core": "[2023.7.2, )",
|
||||||
"Dapper": "2.0.123"
|
"Dapper": "[2.0.123, )"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"infrastructure.entityframework": {
|
"infrastructure.entityframework": {
|
||||||
"type": "Project",
|
"type": "Project",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"AutoMapper.Extensions.Microsoft.DependencyInjection": "12.0.1",
|
"AutoMapper.Extensions.Microsoft.DependencyInjection": "[12.0.1, )",
|
||||||
"Core": "2023.7.2",
|
"Core": "[2023.7.2, )",
|
||||||
"Microsoft.EntityFrameworkCore.Relational": "7.0.5",
|
"Microsoft.EntityFrameworkCore.Relational": "[7.0.5, )",
|
||||||
"Microsoft.EntityFrameworkCore.SqlServer": "7.0.5",
|
"Microsoft.EntityFrameworkCore.SqlServer": "[7.0.5, )",
|
||||||
"Microsoft.EntityFrameworkCore.Sqlite": "7.0.5",
|
"Microsoft.EntityFrameworkCore.Sqlite": "[7.0.5, )",
|
||||||
"Npgsql.EntityFrameworkCore.PostgreSQL": "7.0.4",
|
"Npgsql.EntityFrameworkCore.PostgreSQL": "[7.0.4, )",
|
||||||
"Pomelo.EntityFrameworkCore.MySql": "7.0.0",
|
"Pomelo.EntityFrameworkCore.MySql": "[7.0.0, )",
|
||||||
"linq2db.EntityFrameworkCore": "7.5.0"
|
"linq2db.EntityFrameworkCore": "[7.5.0, )"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
FROM bitwarden/server:dev
|
FROM bitwardenprod.azurecr.io/server:latest
|
||||||
|
|
||||||
LABEL com.bitwarden.product="bitwarden"
|
LABEL com.bitwarden.product="bitwarden"
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user