diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..ea060dc75 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +**/bin +**/obj +**/node_modules diff --git a/.github/workflows/build-self-host.yml b/.github/workflows/build-self-host.yml index 226c5baf5..cc27c4881 100644 --- a/.github/workflows/build-self-host.yml +++ b/.github/workflows/build-self-host.yml @@ -2,12 +2,181 @@ name: Build Self-Host on: + push: + branches-ignore: + - "l10n_master" + - "gh-pages" + paths-ignore: + - ".github/workflows/**" workflow_dispatch: jobs: - stub: - name: Stub + build-docker: + name: Build Docker image runs-on: ubuntu-22.04 steps: - name: Checkout repo uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 + + ########## Set up Docker ########## + - name: Set up QEMU emulators + uses: docker/setup-qemu-action@e81a89b1732b9c48d79cd809d8d81d79c4647a18 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@8c0edbc76e98fa90f69d9a2c020dcb50019dc325 + + ########## Build Docker Image ########## + - name: Login to Azure - QA Subscription + uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf + with: + creds: ${{ secrets.AZURE_QA_KV_CREDENTIALS }} + + - name: Login to Azure ACR + run: az acr login -n bitwardenqa + + - name: Generate Docker image tag + id: tag + run: | + IMAGE_TAG=$(echo "${GITHUB_REF:11}" | sed "s#/#-#g") # slash safe branch name + if [[ "$IMAGE_TAG" == "master" ]]; then + IMAGE_TAG=dev + fi + + echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT + + - name: Build Docker image + uses: docker/build-push-action@c56af957549030174b10d6867f20e78cfd7debc5 + with: + context: . + file: docker-unified/Dockerfile + platforms: | + linux/amd64, + linux/arm/v7, + linux/arm64/v8 + push: true + tags: bitwardenqa.azurecr.io/self-host:${{ steps.tag.outputs.image_tag }} + + - name: Log out of Docker + run: docker logout + + ########## DockerHub ########## + - name: Login to Azure - Prod Subscription + if: | + false + && (github.ref == 'refs/heads/master' || + github.ref == 'refs/heads/rc' || + github.ref == 'refs/heads/hotfix-rc') + uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf + with: + creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }} + + - name: Retrieve secrets + if: | + false + && (github.ref == 'refs/heads/master' || + github.ref == 'refs/heads/rc' || + github.ref == 'refs/heads/hotfix-rc') + id: retrieve-secrets + uses: bitwarden/gh-actions/get-keyvault-secrets@c3b3285993151c5af47cefcb3b9134c28ab479af + with: + keyvault: "bitwarden-prod-kv" + secrets: "docker-password, + docker-username, + dct-delegate-2-repo-passphrase, + dct-delegate-2-key" + + - name: Log into Docker + if: | + false + && (github.ref == 'refs/heads/master' || + github.ref == 'refs/heads/rc' || + github.ref == 'refs/heads/hotfix-rc') + 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 Trust + if: | + false + && (github.ref == 'refs/heads/master' || + github.ref == 'refs/heads/rc' || + github.ref == 'refs/heads/hotfix-rc') + env: + DCT_DELEGATION_KEY_ID: "c9bde8ec820701516491e5e03d3a6354e7bd66d05fa3df2b0062f68b116dc59c" + DCT_DELEGATE_KEY: ${{ steps.retrieve-secrets.outputs.dct-delegate-2-key }} + DCT_REPO_PASSPHRASE: ${{ steps.retrieve-secrets.outputs.dct-delegate-2-repo-passphrase }} + run: | + mkdir -p ~/.docker/trust/private + echo "$DCT_DELEGATE_KEY" > ~/.docker/trust/private/$DCT_DELEGATION_KEY_ID.key + echo "DOCKER_CONTENT_TRUST=1" >> $GITHUB_ENV + echo "DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE=$DCT_REPO_PASSPHRASE" >> $GITHUB_ENV + + - name: Tag and Push RC to Docker Hub + if: | + false + && (github.ref == 'refs/heads/master' || + github.ref == 'refs/heads/rc' || + github.ref == 'refs/heads/hotfix-rc') + env: + PROJECT_NAME: self-host + REGISTRY: bitwarden + run: | + IMAGE_TAG=$(echo "${GITHUB_REF:11}" | sed "s#/#-#g") # slash safe branch name + if [[ "$IMAGE_TAG" == "master" ]]; then + IMAGE_TAG=dev + fi + + docker tag $PROJECT_NAME \ + $REGISTRY/$PROJECT_NAME:$IMAGE_TAG + docker push $REGISTRY/$PROJECT_NAME:$IMAGE_TAG + + - name: Log out of Docker and disable Docker Notary + if: | + false + && (github.ref == 'refs/heads/master' || + github.ref == 'refs/heads/rc' || + github.ref == 'refs/heads/hotfix-rc') + run: | + docker logout + echo "DOCKER_CONTENT_TRUST=0" >> $GITHUB_ENV + + check-failures: + name: Check for failures + if: always() + runs-on: ubuntu-22.04 + needs: build-docker + steps: + - name: Check if any job failed + if: | + github.ref == 'refs/heads/master' + || github.ref == 'refs/heads/rc' + || github.ref == 'refs/heads/hotfix-rc' + env: + BUILD_DOCKER_STATUS: ${{ needs.build-docker.result }} + run: | + if [ "$BUILD_DOCKER_STATUS" = "failure" ]; then + exit 1 + fi + + - name: Login to Azure - Prod Subscription + uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf + if: failure() + with: + creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }} + + - name: Retrieve secrets + id: retrieve-secrets + uses: bitwarden/gh-actions/get-keyvault-secrets@c3b3285993151c5af47cefcb3b9134c28ab479af + if: failure() + with: + keyvault: "bitwarden-prod-kv" + secrets: "devops-alerts-slack-webhook-url" + + - name: Notify Slack on failure + uses: act10ns/slack@da3191ebe2e67f49b46880b4633f5591a96d1d33 + if: failure() + env: + SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }} + with: + status: ${{ job.status }} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3a8a9053e..f59db532d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,12 +9,11 @@ on: paths-ignore: - ".github/workflows/**" workflow_dispatch: - inputs: {} jobs: cloc: name: CLOC - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Checkout repo uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b @@ -29,7 +28,7 @@ jobs: lint: name: Lint - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Checkout repo uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 @@ -83,7 +82,7 @@ jobs: build-artifacts: name: Build artifacts - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 needs: - testing - lint @@ -91,31 +90,31 @@ jobs: fail-fast: false matrix: include: - - service_name: Admin + - project_name: Admin base_path: ./src node: true - - service_name: Api + - project_name: Api base_path: ./src - - service_name: Billing + - project_name: Billing base_path: ./src - - service_name: Events + - project_name: Events base_path: ./src - - service_name: EventsProcessor + - project_name: EventsProcessor base_path: ./src - - service_name: Icons + - project_name: Icons base_path: ./src - - service_name: Identity + - project_name: Identity base_path: ./src - - service_name: Notifications + - project_name: Notifications base_path: ./src - - service_name: Server + - project_name: Server base_path: ./util - - service_name: Setup + - project_name: Setup base_path: ./util - - service_name: Sso + - project_name: Sso base_path: ./bitwarden_license/src node: true - - service_name: Scim + - project_name: Scim base_path: ./bitwarden_license/src dotnet: true steps: @@ -138,8 +137,8 @@ jobs: echo "GitHub ref: $GITHUB_REF" echo "GitHub event: $GITHUB_EVENT" - - name: Restore/Clean service - working-directory: ${{ matrix.base_path }}/${{ matrix.service_name }} + - name: Restore/Clean project + working-directory: ${{ matrix.base_path }}/${{ matrix.project_name }} run: | echo "Restore" dotnet restore @@ -148,92 +147,89 @@ jobs: - name: Build node if: ${{ matrix.node }} - working-directory: ${{ matrix.base_path }}/${{ matrix.service_name }} + working-directory: ${{ matrix.base_path }}/${{ matrix.project_name }} run: | npm ci npm run build - - name: Publish service - working-directory: ${{ matrix.base_path }}/${{ matrix.service_name }} + - name: Publish project + working-directory: ${{ matrix.base_path }}/${{ matrix.project_name }} run: | echo "Publish" dotnet publish -c "Release" -o obj/build-output/publish cd obj/build-output/publish - zip -r ${{ matrix.service_name }}.zip . - mv ${{ matrix.service_name }}.zip ../../../ + zip -r ${{ matrix.project_name }}.zip . + mv ${{ matrix.project_name }}.zip ../../../ pwd ls -atlh ../../../ - - name: Upload service artifact + - name: Upload project artifact uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535 with: - name: ${{ matrix.service_name }}.zip - path: ${{ matrix.base_path }}/${{ matrix.service_name }}/${{ matrix.service_name }}.zip + name: ${{ matrix.project_name }}.zip + path: ${{ matrix.base_path }}/${{ matrix.project_name }}/${{ matrix.project_name }}.zip if-no-files-found: error build-docker: name: Build Docker images - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 needs: build-artifacts strategy: fail-fast: false matrix: include: - - service_name: Admin + - project_name: Admin base_path: ./src docker_repos: [bitwarden, bitwardenqa.azurecr.io] dotnet: true - - service_name: Api + - project_name: Api base_path: ./src docker_repos: [bitwarden, bitwardenqa.azurecr.io] dotnet: true - - service_name: Attachments + - project_name: Attachments base_path: ./util docker_repos: [bitwarden, bitwardenqa.azurecr.io] - - service_name: Events + - project_name: Events base_path: ./src docker_repos: [bitwarden, bitwardenqa.azurecr.io] dotnet: true - - service_name: EventsProcessor + - project_name: EventsProcessor base_path: ./src docker_repos: [bitwardenqa.azurecr.io] dotnet: true - - service_name: Icons + - project_name: Icons base_path: ./src docker_repos: [bitwarden, bitwardenqa.azurecr.io] dotnet: true - - service_name: Identity + - project_name: Identity base_path: ./src docker_repos: [bitwarden, bitwardenqa.azurecr.io] dotnet: true - - service_name: K8S-Proxy + - project_name: MsSql base_path: ./util docker_repos: [bitwarden, bitwardenqa.azurecr.io] - - service_name: MsSql + - project_name: Nginx base_path: ./util docker_repos: [bitwarden, bitwardenqa.azurecr.io] - - service_name: Nginx - base_path: ./util - docker_repos: [bitwarden, bitwardenqa.azurecr.io] - - service_name: Notifications + - project_name: Notifications base_path: ./src docker_repos: [bitwarden, bitwardenqa.azurecr.io] dotnet: true - - service_name: Server + - project_name: Server base_path: ./util docker_repos: [bitwarden, bitwardenqa.azurecr.io] dotnet: true - - service_name: Setup + - project_name: Setup base_path: ./util docker_repos: [bitwarden, bitwardenqa.azurecr.io] dotnet: true - - service_name: Sso + - project_name: Sso base_path: ./bitwarden_license/src docker_repos: [bitwarden, bitwardenqa.azurecr.io] dotnet: true - - service_name: Scim + - project_name: Scim base_path: ./bitwarden_license/src docker_repos: [bitwarden, bitwardenqa.azurecr.io] dotnet: true @@ -243,36 +239,31 @@ jobs: uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 ########## Build Docker Image ########## - - name: Setup service name + - name: Setup project name id: setup run: | - SERVICE_NAME=$(echo "${{ matrix.service_name }}" | awk '{print tolower($0)}') - echo "Matrix name: ${{ matrix.service_name }}" - echo "SERVICE_NAME: $SERVICE_NAME" - echo "::set-output name=service_name::$SERVICE_NAME" + PROJECT_NAME=$(echo "${{ matrix.project_name }}" | awk '{print tolower($0)}') + echo "Matrix name: ${{ matrix.project_name }}" + echo "PROJECT_NAME: $PROJECT_NAME" + echo "::set-output name=project_name::$PROJECT_NAME" - name: Get build artifact if: ${{ matrix.dotnet }} uses: actions/download-artifact@fb598a63ae348fa914e94cd0ff38f362e927b741 with: - name: ${{ matrix.service_name }}.zip + name: ${{ matrix.project_name }}.zip - name: Setup build artifact if: ${{ matrix.dotnet }} run: | - mkdir -p ${{ matrix.base_path}}/${{ matrix.service_name }}/obj/build-output/publish - unzip ${{ matrix.service_name }}.zip \ - -d ${{ matrix.base_path }}/${{ matrix.service_name }}/obj/build-output/publish + mkdir -p ${{ matrix.base_path}}/${{ matrix.project_name }}/obj/build-output/publish + unzip ${{ matrix.project_name }}.zip \ + -d ${{ matrix.base_path }}/${{ matrix.project_name }}/obj/build-output/publish - - name: Build Docker images - run: | - if [ "${{ matrix.service_name }}" = "K8S-Proxy" ]; then - docker build -f ${{ matrix.base_path }}/Nginx/Dockerfile-k8s \ - -t ${{ steps.setup.outputs.service_name }} ${{ matrix.base_path }}/Nginx - else - docker build -t ${{ steps.setup.outputs.service_name }} \ - ${{ matrix.base_path }}/${{ matrix.service_name }} - fi + - name: Build Docker image + env: + PROJECT_NAME: ${{ steps.setup.outputs.project_name }} + run: docker build -t $PROJECT_NAME ${{ matrix.base_path }}/${{ matrix.project_name }} ########## ACR ########## - name: Login to Azure - QA Subscription @@ -283,8 +274,9 @@ jobs: - name: Login to Azure ACR run: az acr login -n bitwardenqa - - name: Tag and Push RC to Azure ACR QA registry + - name: Tag and Push image to Azure ACR QA registry env: + PROJECT_NAME: ${{ steps.setup.outputs.project_name }} REGISTRY: bitwardenqa.azurecr.io run: | IMAGE_TAG=$(echo "${GITHUB_REF:11}" | sed "s#/#-#g") # slash safe branch name @@ -292,9 +284,9 @@ jobs: IMAGE_TAG=dev fi - docker tag ${{ steps.setup.outputs.service_name }} \ - $REGISTRY/${{ steps.setup.outputs.service_name }}:$IMAGE_TAG - docker push $REGISTRY/${{ steps.setup.outputs.service_name }}:$IMAGE_TAG + docker tag $PROJECT_NAME \ + $REGISTRY/$PROJECT_NAME:$IMAGE_TAG + docker push $REGISTRY/$PROJECT_NAME:$IMAGE_TAG - name: Log out of Docker run: docker logout @@ -360,6 +352,7 @@ jobs: github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc') env: + PROJECT_NAME: ${{ steps.setup.outputs.project_name }} REGISTRY: bitwarden run: | IMAGE_TAG=$(echo "${GITHUB_REF:11}" | sed "s#/#-#g") # slash safe branch name @@ -367,9 +360,9 @@ jobs: IMAGE_TAG=dev fi - docker tag ${{ steps.setup.outputs.service_name }} \ - $REGISTRY/${{ steps.setup.outputs.service_name }}:$IMAGE_TAG - docker push $REGISTRY/${{ steps.setup.outputs.service_name }}:$IMAGE_TAG + docker tag $PROJECT_NAME \ + $REGISTRY/$PROJECT_NAME:$IMAGE_TAG + docker push $REGISTRY/$PROJECT_NAME:$IMAGE_TAG - name: Log out of Docker and disable Docker Notary if: | @@ -383,7 +376,7 @@ jobs: upload: name: Upload - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 needs: build-docker steps: - name: Checkout repo @@ -462,7 +455,7 @@ jobs: check-failures: name: Check for failures if: always() - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 needs: - cloc - lint diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 17d3c2367..f7621b8e7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,20 +6,19 @@ on: workflow_dispatch: inputs: release_type: - description: 'Release Options' + description: "Release Options" required: true - default: 'Initial Release' + default: "Initial Release" type: choice options: - Initial Release - Redeploy - Dry Run - jobs: setup: name: Setup - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 outputs: release_version: ${{ steps.version.outputs.version }} branch-name: ${{ steps.branch.outputs.branch-name }} @@ -51,10 +50,9 @@ jobs: BRANCH_NAME=$(basename ${{ github.ref }}) echo "::set-output name=branch-name::$BRANCH_NAME" - deploy: name: Deploy - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 needs: - setup strategy: @@ -81,11 +79,11 @@ jobs: uses: chrnorm/deployment-action@1b599fe41a0ef1f95191e7f2eec4743f2d7dfc48 id: deployment with: - token: '${{ secrets.GITHUB_TOKEN }}' - initial-status: 'in_progress' - environment: 'Production Cloud' - task: 'deploy' - description: 'Deploy from ${{ needs.setup.outputs.branch-name }} branch' + token: "${{ secrets.GITHUB_TOKEN }}" + initial-status: "in_progress" + environment: "Production Cloud" + task: "deploy" + description: "Deploy from ${{ needs.setup.outputs.branch-name }} branch" - name: Download latest Release ${{ matrix.name }} asset if: ${{ github.event.inputs.release_type != 'Dry Run' }} @@ -155,22 +153,21 @@ jobs: if: ${{ github.event.inputs.release_type != 'Dry Run' && success() }} uses: chrnorm/deployment-status@07b3930847f65e71c9c6802ff5a402f6dfb46b86 with: - token: '${{ secrets.GITHUB_TOKEN }}' - state: 'success' + token: "${{ secrets.GITHUB_TOKEN }}" + state: "success" deployment-id: ${{ steps.deployment.outputs.deployment_id }} - name: Update ${{ matrix.name }} deployment status to Failure if: ${{ github.event.inputs.release_type != 'Dry Run' && failure() }} uses: chrnorm/deployment-status@07b3930847f65e71c9c6802ff5a402f6dfb46b86 with: - token: '${{ secrets.GITHUB_TOKEN }}' - state: 'failure' + token: "${{ secrets.GITHUB_TOKEN }}" + state: "failure" deployment-id: ${{ steps.deployment.outputs.deployment_id }} - release-docker: name: Build Docker images - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 needs: - setup env: @@ -180,38 +177,36 @@ jobs: fail-fast: false matrix: include: - - service_name: Admin + - project_name: Admin origin_docker_repo: bitwarden - - service_name: Api + - project_name: Api origin_docker_repo: bitwarden - - service_name: Attachments + - project_name: Attachments origin_docker_repo: bitwarden - - service_name: Events + - project_name: Events prod_acr: true origin_docker_repo: bitwarden - - service_name: EventsProcessor + - project_name: EventsProcessor prod_acr: true origin_docker_repo: bitwardenqa.azurecr.io - - service_name: Icons + - project_name: Icons origin_docker_repo: bitwarden prod_acr: true - - service_name: Identity + - project_name: Identity origin_docker_repo: bitwarden - - service_name: K8S-Proxy + - project_name: MsSql origin_docker_repo: bitwarden - - service_name: MsSql + - project_name: Nginx origin_docker_repo: bitwarden - - service_name: Nginx + - project_name: Notifications origin_docker_repo: bitwarden - - service_name: Notifications + - project_name: Server origin_docker_repo: bitwarden - - service_name: Server + - project_name: Setup origin_docker_repo: bitwarden - - service_name: Setup + - project_name: Sso origin_docker_repo: bitwarden - - service_name: Sso - origin_docker_repo: bitwarden - - service_name: Scim + - project_name: Scim origin_docker_repo: bitwarden skip_dct: true steps: @@ -228,13 +223,13 @@ jobs: - name: Checkout repo uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 - - name: Setup service name + - name: Setup project name id: setup run: | - SERVICE_NAME=$(echo "${{ matrix.service_name }}" | awk '{print tolower($0)}') - echo "Matrix name: ${{ matrix.service_name }}" - echo "SERVICE_NAME: $SERVICE_NAME" - echo "::set-output name=service_name::$SERVICE_NAME" + PROJECT_NAME=$(echo "${{ matrix.project_name }}" | awk '{print tolower($0)}') + echo "Matrix name: ${{ matrix.project_name }}" + echo "PROJECT_NAME: $PROJECT_NAME" + echo "::set-output name=project_name::$PROJECT_NAME" ########## DockerHub ########## - name: Setup DCT @@ -255,26 +250,26 @@ jobs: echo "::set-output name=dct_enabled::1" fi - - name: Pull latest selfhost image + - name: Pull latest project image if: matrix.origin_docker_repo == 'bitwarden' env: - SERVICE_NAME: ${{ steps.setup.outputs.service_name }} + PROJECT_NAME: ${{ steps.setup.outputs.project_name }} run: | if [[ "${{ github.event.inputs.release_type }}" == "Dry Run" ]]; then - docker pull bitwarden/$SERVICE_NAME:latest + docker pull bitwarden/$PROJECT_NAME:latest else - docker pull bitwarden/$SERVICE_NAME:$_BRANCH_NAME + docker pull bitwarden/$PROJECT_NAME:$_BRANCH_NAME fi - name: Tag version and latest if: matrix.origin_docker_repo == 'bitwarden' env: - SERVICE_NAME: ${{ steps.setup.outputs.service_name }} + PROJECT_NAME: ${{ steps.setup.outputs.project_name }} run: | if [[ "${{ github.event.inputs.release_type }}" == "Dry Run" ]]; then - docker tag bitwarden/$SERVICE_NAME:latest bitwarden/$SERVICE_NAME:dryrun + docker tag bitwarden/$PROJECT_NAME:latest bitwarden/$PROJECT_NAME:dryrun else - docker tag bitwarden/$SERVICE_NAME:$_BRANCH_NAME bitwarden/$SERVICE_NAME:$_RELEASE_VERSION + docker tag bitwarden/$PROJECT_NAME:$_BRANCH_NAME bitwarden/$PROJECT_NAME:$_RELEASE_VERSION fi - name: Push version and latest image @@ -282,8 +277,8 @@ jobs: env: DOCKER_CONTENT_TRUST: ${{ steps.check-matrix-dct.outputs.dct_enabled }} DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.setup-dct.outputs.dct-delegate-repo-passphrase }} - SERVICE_NAME: ${{ steps.setup.outputs.service_name }} - run: docker push bitwarden/$SERVICE_NAME:$_RELEASE_VERSION + 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' @@ -300,36 +295,39 @@ jobs: - name: Login to Azure ACR run: az acr login -n bitwardenqa - - name: Pull latest selfhost image + - name: Pull latest project image if: matrix.origin_docker_repo == 'bitwardenqa.azurecr.io' env: - SERVICE_NAME: ${{ steps.setup.outputs.service_name }} + PROJECT_NAME: ${{ steps.setup.outputs.project_name }} REGISTRY: bitwardenqa.azurecr.io run: | if [[ "${{ github.event.inputs.release_type }}" == "Dry Run" ]]; then - docker pull $REGISTRY/$SERVICE_NAME:latest + docker pull $REGISTRY/$PROJECT_NAME:latest else - docker pull $REGISTRY/$SERVICE_NAME:$_BRANCH_NAME + docker pull $REGISTRY/$PROJECT_NAME:$_BRANCH_NAME fi - name: Tag version and latest env: - SERVICE_NAME: ${{ steps.setup.outputs.service_name }} + PROJECT_NAME: ${{ steps.setup.outputs.project_name }} REGISTRY: bitwardenqa.azurecr.io - ORIGIN_REGISTY: ${{ matrix.origin_docker_repo }} + ORIGIN_REGISTRY: ${{ matrix.origin_docker_repo }} run: | if [[ "${{ github.event.inputs.release_type }}" == "Dry Run" ]]; then - docker tag $ORIGIN_REGISTY/$SERVICE_NAME:latest $REGISTRY/$SERVICE_NAME:dryrun + docker tag $ORIGIN_REGISTRY/$PROJECT_NAME:latest $REGISTRY/$PROJECT_NAME:dryrun else - docker tag $ORIGIN_REGISTY/$SERVICE_NAME:$_BRANCH_NAME $REGISTRY/$SERVICE_NAME:$_RELEASE_VERSION + docker tag $ORIGIN_REGISTRY/$PROJECT_NAME:$_BRANCH_NAME $REGISTRY/$PROJECT_NAME:$_RELEASE_VERSION + docker tag $ORIGIN_REGISTRY/$PROJECT_NAME:$_BRANCH_NAME $REGISTRY/$PROJECT_NAME:latest fi - name: Push version and latest image if: ${{ github.event.inputs.release_type != 'Dry Run' }} env: - SERVICE_NAME: ${{ steps.setup.outputs.service_name }} + PROJECT_NAME: ${{ steps.setup.outputs.project_name }} REGISTRY: bitwardenqa.azurecr.io - run: docker push $REGISTRY/$SERVICE_NAME:$_RELEASE_VERSION + run: | + docker push $REGISTRY/$PROJECT_NAME:latest + docker push $REGISTRY/$PROJECT_NAME:$_RELEASE_VERSION - name: Log out of Docker run: docker logout @@ -348,31 +346,33 @@ jobs: - name: Tag version and latest if: matrix.prod_acr == true env: - SERVICE_NAME: ${{ steps.setup.outputs.service_name }} + PROJECT_NAME: ${{ steps.setup.outputs.project_name }} REGISTRY: bitwardenprod.azurecr.io - ORIGIN_REGISTY: ${{ matrix.origin_docker_repo }} + ORIGIN_REGISTRY: ${{ matrix.origin_docker_repo }} run: | if [[ "${{ github.event.inputs.release_type }}" == "Dry Run" ]]; then - docker tag $ORIGIN_REGISTY/$SERVICE_NAME:latest $REGISTRY/$SERVICE_NAME:dryrun + docker tag $ORIGIN_REGISTRY/$PROJECT_NAME:latest $REGISTRY/$PROJECT_NAME:dryrun else - docker tag $ORIGIN_REGISTY/$SERVICE_NAME:$_BRANCH_NAME $REGISTRY/$SERVICE_NAME:$_RELEASE_VERSION + docker tag $ORIGIN_REGISTRY/$PROJECT_NAME:$_BRANCH_NAME $REGISTRY/$PROJECT_NAME:$_RELEASE_VERSION + docker tag $ORIGIN_REGISTRY/$PROJECT_NAME:$_BRANCH_NAME $REGISTRY/$PROJECT_NAME:latest fi - name: Push version and latest image if: ${{ github.event.inputs.release_type != 'Dry Run' && matrix.prod_acr == true }} env: - SERVICE_NAME: ${{ steps.setup.outputs.service_name }} + PROJECT_NAME: ${{ steps.setup.outputs.project_name }} REGISTRY: bitwardenprod.azurecr.io - run: docker push $REGISTRY/$SERVICE_NAME:$_RELEASE_VERSION + run: | + docker push $REGISTRY/$PROJECT_NAME:$_RELEASE_VERSION + docker push $REGISTRY/$PROJECT_NAME:latest - name: Log out of Docker if: matrix.prod_acr == true run: docker logout - release: name: Create GitHub Release - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 needs: - setup - deploy @@ -385,8 +385,8 @@ jobs: workflow_conclusion: success branch: ${{ needs.setup.outputs.branch-name }} artifacts: "docker-stub.zip, - docker-stub-sha256.txt, - swagger.json" + docker-stub-sha256.txt, + swagger.json" - name: Download latest Release docker-stub if: ${{ github.event.inputs.release_type == 'Dry Run' }} @@ -396,16 +396,16 @@ jobs: workflow_conclusion: success branch: master artifacts: "docker-stub.zip, - docker-stub-sha256.txt, - swagger.json" + docker-stub-sha256.txt, + swagger.json" - name: Create release if: ${{ github.event.inputs.release_type != 'Dry Run' }} uses: ncipollo/release-action@40bb172bd05f266cf9ba4ff965cb61e9ee5f6d01 with: - artifacts: 'docker-stub.zip, - docker-stub-sha256.txt, - swagger.json' + artifacts: "docker-stub.zip, + docker-stub-sha256.txt, + swagger.json" commit: ${{ github.sha }} tag: "v${{ needs.setup.outputs.release_version }}" name: "Version ${{ needs.setup.outputs.release_version }}" diff --git a/bitwarden_license/src/Scim/Startup.cs b/bitwarden_license/src/Scim/Startup.cs index bf6ccc1cb..4aaccd9ed 100644 --- a/bitwarden_license/src/Scim/Startup.cs +++ b/bitwarden_license/src/Scim/Startup.cs @@ -40,7 +40,7 @@ public class Startup StripeConfiguration.MaxNetworkRetries = globalSettings.Stripe.MaxNetworkRetries; // Repositories - services.AddSqlServerRepositories(globalSettings); + services.AddDatabaseRepositories(globalSettings); // Context services.AddScoped(); diff --git a/bitwarden_license/src/Sso/Startup.cs b/bitwarden_license/src/Sso/Startup.cs index 99aa5961f..aeeaa7e6f 100644 --- a/bitwarden_license/src/Sso/Startup.cs +++ b/bitwarden_license/src/Sso/Startup.cs @@ -37,7 +37,7 @@ public class Startup services.AddCustomDataProtectionServices(Environment, globalSettings); // Repositories - services.AddSqlServerRepositories(globalSettings); + services.AddDatabaseRepositories(globalSettings); // Context services.AddScoped(); diff --git a/docker-unified/.env.example b/docker-unified/.env.example new file mode 100644 index 000000000..06a987dc1 --- /dev/null +++ b/docker-unified/.env.example @@ -0,0 +1,3 @@ +COMPOSE_PROJECT_NAME=bitwarden +REGISTRY=bitwarden +TAG=dev diff --git a/docker-unified/Dockerfile b/docker-unified/Dockerfile new file mode 100644 index 000000000..0a205e681 --- /dev/null +++ b/docker-unified/Dockerfile @@ -0,0 +1,291 @@ +############################################### +# Build stage # +############################################### +FROM --platform=$BUILDPLATFORM alpine AS web-setup + +# Add packages +RUN apk add --update-cache \ + curl \ + jq \ + && rm -rf /var/cache/apk/* + +WORKDIR /tmp + +# Download tags from 'clients' repository +RUN curl https://api.github.com/repos/bitwarden/clients/git/refs/tags --output tags.json + +# Grab last tag/release of the 'web' client +RUN cat tags.json | jq -r 'last(.[] | select(.ref|test("refs/tags/web-v[0-9]{4}.[0-9]{1,2}.[0-9]+"))) | .ref | split("/")[2]' > tag.txt + +# Extract the version of the 'web' client +RUN cat tag.txt | grep -o -E "[0-9]{4}\.[0-9]{1,2}\.[0-9]+" > version.txt + +# Download the built release artifact for the 'web' client +RUN TAG=$(cat tag.txt) \ + && VERSION=$(cat version.txt) \ + && curl -L https://github.com/bitwarden/clients/releases/download/$TAG/web-$VERSION-selfhosted-COMMERCIAL.zip -O + +# Unzip the 'web' client to /tmp/build +RUN VERSION=$(cat version.txt) \ + && unzip web-$VERSION-selfhosted-COMMERCIAL.zip + +############################################### +# Build stage # +############################################### +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:6.0 AS dotnet-build + +# Docker buildx supplies the value for this arg +ARG TARGETPLATFORM + +# Determine proper runtime value for .NET +# We put the value in a file to be read by later layers. +RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \ + RID=linux-x64 ; \ + elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \ + RID=linux-arm64 ; \ + elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \ + RID=linux-arm ; \ + fi \ + && echo "RID=$RID" > /tmp/rid.txt + +# Add packages +# RUN apk add --update-cache \ +# npm \ +# && rm -rf /var/cache/apk/* +RUN apt-get update && apt-get install -y \ + npm \ + && rm -rf /var/lib/apt/lists/* + +RUN npm install -g gulp + +# Copy csproj files as distinct layers +WORKDIR /source +COPY src/Admin/*.csproj ./src/Admin/ +COPY src/Api/*.csproj ./src/Api/ +COPY src/Events/*.csproj ./src/Events/ +COPY src/Icons/*.csproj ./src/Icons/ +COPY src/Identity/*.csproj ./src/Identity/ +COPY src/Notifications/*.csproj ./src/Notifications/ +COPY bitwarden_license/src/Sso/*.csproj ./bitwarden_license/src/Sso/ +COPY bitwarden_license/src/Scim/*.csproj ./bitwarden_license/src/Scim/ +COPY src/Core/*.csproj ./src/Core/ +COPY src/Infrastructure.Dapper/*.csproj ./src/Infrastructure.Dapper/ +COPY src/Infrastructure.EntityFramework/*.csproj ./src/Infrastructure.EntityFramework/ +COPY src/SharedWeb/*.csproj ./src/SharedWeb/ +COPY util/Migrator/*.csproj ./util/Migrator/ +COPY util/MySqlMigrations/*.csproj ./util/MySqlMigrations/ +COPY util/PostgresMigrations/*.csproj ./util/PostgresMigrations/ +COPY bitwarden_license/src/Commercial.Core/*.csproj ./bitwarden_license/src/Commercial.Core/ +COPY Directory.Build.props . + +# Restore Admin project dependencies and tools +WORKDIR /source/src/Admin +RUN . /tmp/rid.txt && dotnet restore -r $RID + +# Restore Api project dependencies and tools +WORKDIR /source/src/Api +RUN . /tmp/rid.txt && dotnet restore -r $RID + +# Restore Events project dependencies and tools +WORKDIR /source/src/Events +RUN . /tmp/rid.txt && dotnet restore -r $RID + +# Restore Icons project dependencies and tools +WORKDIR /source/src/Icons +RUN . /tmp/rid.txt && dotnet restore -r $RID + +# Restore Identity project dependencies and tools +WORKDIR /source/src/Identity +RUN . /tmp/rid.txt && dotnet restore -r $RID + +# Restore Notifications project dependencies and tools +WORKDIR /source/src/Notifications +RUN . /tmp/rid.txt && dotnet restore -r $RID + +# Restore Sso project dependencies and tools +WORKDIR /source/bitwarden_license/src/Sso +RUN . /tmp/rid.txt && dotnet restore -r $RID + +# Restore Scim project dependencies and tools +WORKDIR /source/bitwarden_license/src/Scim +RUN . /tmp/rid.txt && dotnet restore -r $RID + +# Copy required project files +WORKDIR /source +COPY src/Admin/. ./src/Admin/ +COPY src/Api/. ./src/Api/ +COPY src/Events/. ./src/Events/ +COPY src/Icons/. ./src/Icons/ +COPY src/Identity/. ./src/Identity/ +COPY src/Notifications/. ./src/Notifications/ +COPY bitwarden_license/src/Sso/. ./bitwarden_license/src/Sso/ +COPY bitwarden_license/src/Scim/. ./bitwarden_license/src/Scim/ +COPY src/Core/. ./src/Core/ +COPY src/Infrastructure.Dapper/. ./src/Infrastructure.Dapper/ +COPY src/Infrastructure.EntityFramework/. ./src/Infrastructure.EntityFramework/ +COPY src/SharedWeb/. ./src/SharedWeb/ +COPY util/Migrator/. ./util/Migrator/ +COPY util/MySqlMigrations/. ./util/MySqlMigrations/ +COPY util/PostgresMigrations/. ./util/PostgresMigrations/ +COPY util/EfShared/. ./util/EfShared/ +COPY bitwarden_license/src/Commercial.Core/. ./bitwarden_license/src/Commercial.Core/ +COPY .git/. ./.git/ + +# Build Admin app +WORKDIR /source/src/Admin +RUN npm install +RUN gulp --gulpfile "gulpfile.js" build +RUN . /tmp/rid.txt && dotnet publish -c release -o /app/Admin --no-restore --no-self-contained -r $RID + +# Build Api app +WORKDIR /source/src/Api +RUN . /tmp/rid.txt && dotnet publish -c release -o /app/Api --no-restore --no-self-contained -r $RID + +# Build Events app +WORKDIR /source/src/Events +RUN . /tmp/rid.txt && dotnet publish -c release -o /app/Events --no-restore --no-self-contained -r $RID + +# Build Icons app +WORKDIR /source/src/Icons +RUN . /tmp/rid.txt && dotnet publish -c release -o /app/Icons --no-restore --no-self-contained -r $RID + +# Build Identity app +WORKDIR /source/src/Identity +RUN . /tmp/rid.txt && dotnet publish -c release -o /app/Identity --no-restore --no-self-contained -r $RID + +# Build Notifications app +WORKDIR /source/src/Notifications +RUN . /tmp/rid.txt && dotnet publish -c release -o /app/Notifications --no-restore --no-self-contained -r $RID + +# Build Sso app +WORKDIR /source/bitwarden_license/src/Sso +RUN npm install +RUN gulp --gulpfile "gulpfile.js" build +RUN . /tmp/rid.txt && dotnet publish -c release -o /app/Sso --no-restore --no-self-contained -r $RID + +# Build Scim app +WORKDIR /source/bitwarden_license/src/Scim +RUN . /tmp/rid.txt && dotnet publish -c release -o /app/Scim --no-restore --no-self-contained -r $RID + +############################################### +# App stage # +############################################### +FROM mcr.microsoft.com/dotnet/aspnet:6.0-alpine +ARG TARGETPLATFORM +LABEL com.bitwarden.product="bitwarden" +LABEL com.bitwarden.project="unified" +ENV ASPNETCORE_ENVIRONMENT=Production +ENV BW_ENABLE_ADMIN=true +ENV BW_ENABLE_API=true +ENV BW_ENABLE_EVENTS=false +ENV BW_ENABLE_ICONS=true +ENV BW_ENABLE_IDENTITY=true +ENV BW_ENABLE_NOTIFICATIONS=true +ENV BW_ENABLE_SCIM=false +ENV BW_ENABLE_SSO=false +ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false +ENV globalSettings__selfHosted="true" +ENV globalSettings__pushRelayBaseUri="https://push.bitwarden.com" +ENV globalSettings__baseServiceUri__internalAdmin="http://localhost:5000" +ENV globalSettings__baseServiceUri__internalApi="http://localhost:5001" +ENV globalSettings__baseServiceUri__internalEvents="http://localhost:5003" +ENV globalSettings__baseServiceUri__internalIcons="http://localhost:5004" +ENV globalSettings__baseServiceUri__internalIdentity="http://localhost:5005" +ENV globalSettings__baseServiceUri__internalNotifications="http://localhost:5006" +ENV globalSettings__baseServiceUri__internalSso="http://localhost:5007" +ENV globalSettings__baseServiceUri__internalScim="http://localhost:5002" +ENV globalSettings__baseServiceUri__internalVault="http://localhost:80" +ENV globalSettings__identityServer__certificatePassword="default_cert_password" +ENV globalSettings__dataProtection__directory="/etc/bitwarden/data-protection" +ENV globalSettings__attachment__baseDirectory="/etc/bitwarden/attachments" +ENV globalSettings__send__baseDirectory="/etc/bitwarden/attachments/send" +ENV globalSettings__licenseDirectory="/etc/bitwarden/licenses" +ENV globalSettings__logDirectoryByProject="false" +ENV globalSettings__logRollBySizeLimit="1073741824" +EXPOSE 80 +EXPOSE 443 + +# Add packages +RUN apk add --update-cache \ + curl \ + icu-libs \ + nginx \ + openssl \ + su-exec \ + supervisor \ + tzdata \ + && rm -rf /var/cache/apk/* + +# Create non-root user to run app +RUN adduser -s /bin/false -D bitwarden + +# Create required directories +RUN mkdir -p /etc/bitwarden/attachments/send +RUN mkdir -p /etc/bitwarden/data-protection +RUN mkdir -p /etc/bitwarden/licenses +RUN mkdir -p /etc/bitwarden/logs +RUN mkdir -p /etc/supervisor +RUN mkdir -p /etc/supervisor.d +RUN mkdir -p /var/log/bitwarden +RUN mkdir -p /var/log/nginx/logs +RUN mkdir -p /app +RUN chown -R bitwarden:bitwarden \ + /app \ + /etc/bitwarden \ + /etc/nginx/http.d \ + /etc/supervisor \ + /etc/supervisor.d \ + /var/lib/nginx \ + /var/log \ + /run + +# Copy all apps from dotnet-build stage +WORKDIR /app +COPY --chown=bitwarden:bitwarden --from=dotnet-build /app ./ + +# Copy Web files from web-setup stage +COPY --chown=bitwarden:bitwarden --from=web-setup /tmp/build /app/Web + +# Set up supervisord +COPY --chown=bitwarden:bitwarden docker-unified/supervisord/*.ini /etc/supervisor.d/ +COPY --chown=bitwarden:bitwarden docker-unified/supervisord/supervisord.conf /etc/supervisor/supervisord.conf +RUN rm -f /etc/supervisord.conf + +# Set up nginx +COPY docker-unified/nginx/nginx.conf /etc/nginx +COPY docker-unified/nginx/proxy.conf /etc/nginx +COPY docker-unified/nginx/mime.types /etc/nginx +COPY docker-unified/nginx/security-headers.conf /etc/nginx +COPY docker-unified/nginx/security-headers-ssl.conf /etc/nginx +COPY docker-unified/nginx/logrotate.sh / +RUN chmod +x /logrotate.sh + +# Copy configuration templates +COPY docker-unified/confd/nginx-config.toml /etc/confd/conf.d/ +COPY docker-unified/confd/nginx-config.conf.tmpl /etc/confd/templates/ +COPY docker-unified/confd/app-id.toml /etc/confd/conf.d/ +COPY docker-unified/confd/app-id.conf.tmpl /etc/confd/templates/ + +# Download confd tool for generating final configurations +RUN if [ "$TARGETPLATFORM" = "linux/amd64" ] ; then curl -L --output confd.tar.gz https://github.com/abtreece/confd/releases/download/v0.19.1/confd-v0.19.1-linux-amd64.tar.gz; fi +RUN if [ "$TARGETPLATFORM" = "linux/arm/v7" ] ; then curl -L --output confd.tar.gz https://github.com/abtreece/confd/releases/download/v0.19.1/confd-v0.19.1-linux-arm7.tar.gz; fi +RUN if [ "$TARGETPLATFORM" = "linux/arm64" ] ; then curl -L --output confd.tar.gz https://github.com/abtreece/confd/releases/download/v0.19.1/confd-v0.19.1-linux-arm64.tar.gz; fi + +# Extract confd +RUN tar -xvzo -C /usr/local/bin -f confd.tar.gz && rm confd.tar.gz + +# Copy entrypoint script and make it executable +COPY docker-unified/entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +# TODO: Remove after testing +RUN apk add --update-cache \ + vim \ + && rm -rf /var/cache/apk/* + +VOLUME ["/etc/bitwarden"] + +WORKDIR /app +USER bitwarden:bitwarden +HEALTHCHECK CMD curl --insecure -Lfs https://localhost/alive || curl -Lfs http://localhost/alive || exit 1 +ENTRYPOINT ["/entrypoint.sh"] diff --git a/docker-unified/confd/app-id.conf.tmpl b/docker-unified/confd/app-id.conf.tmpl new file mode 100644 index 000000000..ef50fcafb --- /dev/null +++ b/docker-unified/confd/app-id.conf.tmpl @@ -0,0 +1,15 @@ +{ + "trustedFacets": [ + { + "version": { + "major": 1, + "minor": 0 + }, + "ids": [ + "{{ getenv "globalSettings__baseServiceUri__vault" "https://localhost" }}", + "ios:bundle-id:com.8bit.bitwarden", + "android:apk-key-hash:dUGFzUzf3lmHSLBDBIv+WaFyZMI" + ] + } + ] +} diff --git a/docker-unified/confd/app-id.toml b/docker-unified/confd/app-id.toml new file mode 100644 index 000000000..701c208bb --- /dev/null +++ b/docker-unified/confd/app-id.toml @@ -0,0 +1,6 @@ +[template] +src = "app-id.conf.tmpl" +dest = "/app/Web/app-id.json" +keys = [ + "globalSettings__baseServiceUri__vault" +] diff --git a/docker-unified/confd/nginx-config.conf.tmpl b/docker-unified/confd/nginx-config.conf.tmpl new file mode 100644 index 000000000..43730181f --- /dev/null +++ b/docker-unified/confd/nginx-config.conf.tmpl @@ -0,0 +1,144 @@ +server { + listen 80 default_server; + #listen [::]:80 default_server; + server_name {{ getenv "BW_DOMAIN" "localhost" }}; +{{ if eq (getenv "BW_ENABLE_SSL") "true" }} + + return 301 https://{{ getenv "BW_DOMAIN" "localhost" }}$request_uri; +} + +server { + listen 443 ssl http2; + #listen [::]:443 ssl http2; + server_name {{ getenv "BW_DOMAIN" "localhost" }}; + + ssl_certificate /etc/bitwarden/{{ getenv "BW_SSL_CERT" "ssl.crt" }}; + ssl_certificate_key /etc/bitwarden/{{ getenv "BW_SSL_KEY" "ssl.key" }}; + ssl_session_timeout 30m; + ssl_session_cache shared:SSL:20m; + ssl_session_tickets off; + + ssl_protocols {{ getenv "BW_SSL_PROTOCOLS" "TLSv1.2" }}; + ssl_ciphers "{{ getenv "BW_SSL_CIPHERS" "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256" }}"; + # Enables server-side protection from BEAST attacks + ssl_prefer_server_ciphers on; +{{ if eq (getenv "BW_ENABLE_SSL_CA") "true" }} + + # OCSP Stapling --- + # Fetch OCSP records from URL in ssl_certificate and cache them + ssl_stapling on; + ssl_stapling_verify on; + + # Verify chain of trust of OCSP response using Root CA and Intermediate certs + ssl_trusted_certificate /etc/bitwarden/{{ getenv "BW_SSL_CA_CERT" "ca.crt" }}; + resolver 1.1.1.1 1.0.0.1 9.9.9.9 149.112.112.112 valid=300s; +{{ end }} + + include /etc/nginx/security-headers-ssl.conf; +{{ end }} + include /etc/nginx/security-headers.conf; +{{ if getenv "BW_REAL_IPS" }} + +{{ range (getenv "BW_REAL_IPS") }} + set_real_ip_from {{ .Key }}; +{{ end }} + real_ip_header X-Forwarded-For; + real_ip_recursive on; +{{ end }} + + location / { + root /app/Web; +{{ if eq (getenv "BW_ENABLE_SSL") "true" }} + include /etc/nginx/security-headers-ssl.conf; +{{ end }} + include /etc/nginx/security-headers.conf; + add_header Content-Security-Policy "{{ getenv "BW_CSP" "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://haveibeenpwned.com https://www.gravatar.com; child-src 'self' https://*.duosecurity.com https://*.duofederal.com; frame-src 'self' https://*.duosecurity.com https://*.duofederal.com; connect-src 'self' https://api.pwnedpasswords.com https://2fa.directory; object-src 'self' blob:;" }}"; + add_header X-Frame-Options SAMEORIGIN; + add_header X-Robots-Tag "noindex, nofollow"; + } + + location /alive { + default_type text/plain; + return 200 $date_gmt; + } + + location = /app-id.json { + root /app/Web; +{{ if eq (getenv "BW_ENABLE_SSL") "true" }} + include /etc/nginx/security-headers-ssl.conf; +{{ end }} + include /etc/nginx/security-headers.conf; + proxy_hide_header Content-Type; + add_header Content-Type $fido_content_type; + } + + location /attachments { + alias /etc/bitwarden/attachments/; + } + + location /api/ { + proxy_pass http://localhost:5001/; + } + + location /icons/ { +{{ if eq (getenv "BW_ICONS_PROXY_TO_CLOUD") "true" }} + proxy_pass https://icons.bitwarden.net/; + proxy_set_header Host icons.bitwarden.net; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_ssl_server_name on; +{{ else }} + proxy_pass http://localhost:5004/; +{{ end }} + } + + location /notifications/ { + proxy_pass http://localhost:5006/; + } + + location /notifications/hub { + proxy_pass http://localhost:5006/hub; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $http_connection; + } + + location /events/ { + proxy_pass http://localhost:5003/; + } + + location /sso { + proxy_pass http://localhost:5007; +{{ if eq (getenv "BW_ENABLE_SSL") "true" }} + include /etc/nginx/security-headers-ssl.conf; +{{ end }} + include /etc/nginx/security-headers.conf; + add_header X-Frame-Options SAMEORIGIN; + } + + location /identity { + proxy_pass http://localhost:5005; +{{ if eq (getenv "BW_ENABLE_SSL") "true" }} + include /etc/nginx/security-headers-ssl.conf; +{{ end }} + include /etc/nginx/security-headers.conf; + add_header X-Frame-Options SAMEORIGIN; + } + + location /admin { + proxy_pass http://localhost:5000; +{{ if eq (getenv "BW_ENABLE_SSL") "true" }} + include /etc/nginx/security-headers-ssl.conf; +{{ end }} + include /etc/nginx/security-headers.conf; + add_header X-Frame-Options SAMEORIGIN; + } + +{{ if eq (getenv "BW_ENABLE_KEY_CONNECTOR") "true" }} + location /key-connector/ { + proxy_pass {{ getenv "BW_KEY_CONNECTOR_INTERNAL_URL"}}/; + } +{{ end }} + + location /scim/ { + proxy_pass http://localhost:5002/; + } +} diff --git a/docker-unified/confd/nginx-config.toml b/docker-unified/confd/nginx-config.toml new file mode 100644 index 000000000..c3693c401 --- /dev/null +++ b/docker-unified/confd/nginx-config.toml @@ -0,0 +1,17 @@ +[template] +src = "nginx-config.conf.tmpl" +dest = "/etc/nginx/http.d/bitwarden.conf" +keys = [ + "BW_ENABLE_SSL_CA", + "BW_SSL_CA_CERT", + "BW_CSP", + "BW_ENABLE_KEY_CONNECTOR", + "BW_KEY_CONNECTOR_INTERNAL_URL", + "BW_REAL_IPS", + "BW_ENABLE_SSL", + "BW_SSL_CERT", + "BW_SSL_CIPHERS", + "BW_SSL_KEY", + "BW_SSL_PROTOCOLS", + "BW_ICONS_PROXY_TO_CLOUD" +] diff --git a/docker-unified/docker-compose.yml b/docker-unified/docker-compose.yml new file mode 100644 index 000000000..ad9119fe6 --- /dev/null +++ b/docker-unified/docker-compose.yml @@ -0,0 +1,56 @@ +--- +version: "3.8" + +services: + bitwarden: + depends_on: + - db + env_file: + - settings.env + image: ${REGISTRY:-bitwarden}/self-host:${TAG:-latest} + restart: always + ports: + - "80:80" + - "443:443" + volumes: + - bitwarden:/etc/bitwarden + - logs:/var/log/bitwarden + + # MariaDB Example + db: + environment: + MARIADB_USER: "bitwarden" + MARIADB_PASSWORD: "super_strong_password" + MARIADB_DATABASE: "bitwarden_vault" + MARIADB_RANDOM_ROOT_PASSWORD: "true" + image: mariadb:10 + restart: always + volumes: + - data:/var/lib/mysql + + # PostgreSQL Example + # db: + # environment: + # POSTGRES_USER: "bitwarden" + # POSTGRES_PASSWORD: "super_strong_password" + # POSTGRES_DB: "bitwarden_vault" + # image: postgres:15 + # restart: always + # volumes: + # - data:/var/lib/postgresql/data + + # MS SQL Server Example + # Docs: https://learn.microsoft.com/en-us/sql/linux/sql-server-linux-docker-container-deployment + # db: + # environment: + # MSSQL_SA_PASSWORD: "super_strong_password" + # ACCEPT_EULA: Y + # image: mcr.microsoft.com/mssql/server:2019-latest + # restart: always + # volumes: + # - data:/var/opt/mssql/data + +volumes: + bitwarden: + logs: + data: diff --git a/docker-unified/entrypoint.sh b/docker-unified/entrypoint.sh new file mode 100644 index 000000000..a3e8af82e --- /dev/null +++ b/docker-unified/entrypoint.sh @@ -0,0 +1,81 @@ +#!/bin/sh + +# Translate environment variables for application settings +VAULT_SERVICE_URI=https://$BW_DOMAIN +MYSQL_CONNECTION_STRING="server=$BW_DB_SERVER;database=$BW_DB_DATABASE;user=$BW_DB_USERNAME;password=$BW_DB_PASSWORD" +POSTGRESQL_CONNECTION_STRING="Host=$BW_DB_SERVER;Database=$BW_DB_DATABASE;Username=$BW_DB_USERNAME;Password=$BW_DB_PASSWORD" +SQLSERVER_CONNECTION_STRING="Server=$BW_DB_SERVER;Database=$BW_DB_DATABASE;User Id=$BW_DB_USERNAME;Password=$BW_DB_PASSWORD;" +INTERNAL_IDENTITY_KEY=$(openssl rand -hex 30) +OIDC_IDENTITY_CLIENT_KEY=$(openssl rand -hex 30) +DUO_AKEY=$(openssl rand -hex 30) + +export globalSettings__baseServiceUri__vault=${globalSettings__baseServiceUri__vault:-$VAULT_SERVICE_URI} +export globalSettings__installation__id=$BW_INSTALLATION_ID +export globalSettings__installation__key=$BW_INSTALLATION_KEY +export globalSettings__internalIdentityKey=${globalSettings__internalIdentityKey:-$INTERNAL_IDENTITY_KEY} +export globalSettings__oidcIdentityClientKey=${globalSettings__oidcIdentityClientKey:-$OIDC_IDENTITY_CLIENT_KEY} +export globalSettings__duo__aKey=${globalSettings__duo__aKey:-$DUO_AKEY} + +export globalSettings__databaseProvider=$BW_DB_PROVIDER +export globalSettings__mysql__connectionString=${globalSettings__mysql__connectionString:-$MYSQL_CONNECTION_STRING} +export globalSettings__postgreSql__connectionString=${globalSettings__postgreSql__connectionString:-$POSTGRESQL_CONNECTION_STRING} +export globalSettings__sqlServer__connectionString=${globalSettings__sqlServer__connectionString:-$SQLSERVER_CONNECTION_STRING} + +# Generate Identity certificate +if [ ! -f /etc/bitwarden/identity.pfx ]; then + openssl req \ + -x509 \ + -newkey rsa:4096 \ + -sha256 \ + -nodes \ + -keyout /etc/bitwarden/identity.key \ + -out /etc/bitwarden/identity.crt \ + -subj "/CN=Bitwarden IdentityServer" \ + -days 36500 + + openssl pkcs12 \ + -export \ + -out /etc/bitwarden/identity.pfx \ + -inkey /etc/bitwarden/identity.key \ + -in /etc/bitwarden/identity.crt \ + -passout pass:$globalSettings__identityServer__certificatePassword + + rm /etc/bitwarden/identity.crt + rm /etc/bitwarden/identity.key +fi + +cp /etc/bitwarden/identity.pfx /app/Identity/identity.pfx +cp /etc/bitwarden/identity.pfx /app/Sso/identity.pfx + +# Generate SSL certificates +if [ "$BW_ENABLE_SSL" == "true" -a ! -f /etc/bitwarden/ssl.key ]; then + openssl req \ + -x509 \ + -newkey rsa:4096 \ + -sha256 \ + -nodes \ + -days 36500 \ + -keyout /etc/bitwarden/${BW_SSL_KEY:-ssl.key} \ + -out /etc/bitwarden/${BW_SSL_CERT:-ssl.crt} \ + -reqexts SAN \ + -extensions SAN \ + -config <(cat /etc/ssl/openssl.cnf <(printf "[SAN]\nsubjectAltName=DNS:${BW_DOMAIN:-localhost}\nbasicConstraints=CA:true")) \ + -subj "/C=US/ST=California/L=Santa Barbara/O=Bitwarden Inc./OU=Bitwarden/CN=${BW_DOMAIN:-localhost}" +fi + +# Launch a loop to rotate nginx logs on a daily basis +/bin/sh -c "/logrotate.sh loop >/dev/null 2>&1 &" + +/usr/local/bin/confd -onetime -backend env + +# Enable/Disable services +sed -i "s/autostart=true/autostart=${BW_ENABLE_ADMIN}/" /etc/supervisor.d/admin.ini +sed -i "s/autostart=true/autostart=${BW_ENABLE_API}/" /etc/supervisor.d/api.ini +sed -i "s/autostart=true/autostart=${BW_ENABLE_EVENTS}/" /etc/supervisor.d/events.ini +sed -i "s/autostart=true/autostart=${BW_ENABLE_ICONS}/" /etc/supervisor.d/icons.ini +sed -i "s/autostart=true/autostart=${BW_ENABLE_IDENTITY}/" /etc/supervisor.d/identity.ini +sed -i "s/autostart=true/autostart=${BW_ENABLE_NOTIFICATIONS}/" /etc/supervisor.d/notifications.ini +sed -i "s/autostart=true/autostart=${BW_ENABLE_SCIM}/" /etc/supervisor.d/scim.ini +sed -i "s/autostart=true/autostart=${BW_ENABLE_SSO}/" /etc/supervisor.d/sso.ini + +exec /usr/bin/supervisord diff --git a/docker-unified/nginx/logrotate.sh b/docker-unified/nginx/logrotate.sh new file mode 100644 index 000000000..d86c79c5d --- /dev/null +++ b/docker-unified/nginx/logrotate.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +while true +do + [ "$1" = "loop" ] && sleep $((24 * 3600 - (`date +%_H` * 3600 + `date +%_M` * 60 + `date +%_S`))) + ts=$(date +%Y%m%d_%H%M%S) + mv /var/log/nginx/access.log /var/log/nginx/access.$ts.log + mv /var/log/nginx/error.log /var/log/nginx/error.$ts.log + kill -USR1 `cat /var/run/nginx/nginx.pid` + sleep 1 + gzip /var/log/nginx/access.$ts.log + gzip /var/log/nginx/error.$ts.log + find /var/log/nginx/ -name "*.gz" -mtime +32 -delete + [ "$1" != "loop" ] && break +done diff --git a/docker-unified/nginx/mime.types b/docker-unified/nginx/mime.types new file mode 100644 index 000000000..7c3b1e738 --- /dev/null +++ b/docker-unified/nginx/mime.types @@ -0,0 +1,138 @@ +types { + + # Data interchange + + application/atom+xml atom; + application/json json map topojson; + application/ld+json jsonld; + application/rss+xml rss; + application/vnd.geo+json geojson; + application/xml rdf xml; + + + # JavaScript + + # Normalize to standard type. + # https://tools.ietf.org/html/rfc4329#section-7.2 + application/javascript js; + + + # Manifest files + + application/manifest+json webmanifest; + application/x-web-app-manifest+json webapp; + text/cache-manifest appcache; + + + # Media files + + audio/midi mid midi kar; + audio/mp4 aac f4a f4b m4a; + audio/mpeg mp3; + audio/ogg oga ogg opus; + audio/x-realaudio ra; + audio/x-wav wav; + image/bmp bmp; + image/gif gif; + image/jpeg jpeg jpg; + image/jxr jxr hdp wdp; + image/png png; + image/svg+xml svg svgz; + image/tiff tif tiff; + image/vnd.wap.wbmp wbmp; + image/webp webp; + image/x-jng jng; + video/3gpp 3gp 3gpp; + video/mp4 f4p f4v m4v mp4; + video/mpeg mpeg mpg; + video/ogg ogv; + video/quicktime mov; + video/webm webm; + video/x-flv flv; + video/x-mng mng; + video/x-ms-asf asf asx; + video/x-ms-wmv wmv; + video/x-msvideo avi; + + # Serving `.ico` image files with a different media type + # prevents Internet Explorer from displaying then as images: + # https://github.com/h5bp/html5-boilerplate/commit/37b5fec090d00f38de64b591bcddcb205aadf8ee + + image/x-icon cur ico; + + + # Microsoft Office + + application/msword doc; + application/vnd.ms-excel xls; + application/vnd.ms-powerpoint ppt; + application/vnd.openxmlformats-officedocument.wordprocessingml.document docx; + application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlsx; + application/vnd.openxmlformats-officedocument.presentationml.presentation pptx; + + + # Web fonts + + application/font-woff woff; + application/font-woff2 woff2; + application/vnd.ms-fontobject eot; + + # Browsers usually ignore the font media types and simply sniff + # the bytes to figure out the font type. + # https://mimesniff.spec.whatwg.org/#matching-a-font-type-pattern + # + # However, Blink and WebKit based browsers will show a warning + # in the console if the following font types are served with any + # other media types. + + application/x-font-ttf ttc ttf; + font/opentype otf; + + + # Other + + application/java-archive ear jar war; + application/mac-binhex40 hqx; + application/octet-stream bin deb dll dmg exe img iso msi msm msp safariextz; + application/pdf pdf; + application/postscript ai eps ps; + application/rtf rtf; + application/vnd.google-earth.kml+xml kml; + application/vnd.google-earth.kmz kmz; + application/vnd.wap.wmlc wmlc; + application/x-7z-compressed 7z; + application/x-bb-appworld bbaw; + application/x-bittorrent torrent; + application/x-chrome-extension crx; + application/x-cocoa cco; + application/x-java-archive-diff jardiff; + application/x-java-jnlp-file jnlp; + application/x-makeself run; + application/x-opera-extension oex; + application/x-perl pl pm; + application/x-pilot pdb prc; + application/x-rar-compressed rar; + application/x-redhat-package-manager rpm; + application/x-sea sea; + application/x-shockwave-flash swf; + application/x-stuffit sit; + application/x-tcl tcl tk; + application/x-x509-ca-cert crt der pem; + application/x-xpinstall xpi; + application/xhtml+xml xhtml; + application/xslt+xml xsl; + application/zip zip; + text/css css; + text/csv csv; + text/html htm html shtml; + text/markdown md; + text/mathml mml; + text/plain txt; + text/vcard vcard vcf; + text/vnd.rim.location.xloc xloc; + text/vnd.sun.j2me.app-descriptor jad; + text/vnd.wap.wml wml; + text/vtt vtt; + text/x-component htc; + +} diff --git a/docker-unified/nginx/nginx.conf b/docker-unified/nginx/nginx.conf new file mode 100644 index 000000000..93445e8a2 --- /dev/null +++ b/docker-unified/nginx/nginx.conf @@ -0,0 +1,147 @@ +# nginx Configuration File +# http://wiki.nginx.org/Configuration + +daemon off; + +# Run as a less privileged user for security reasons. +# user www www; + +# How many worker threads to run; +# "auto" sets it to the number of CPU cores available in the system, and +# offers the best performance. Don't set it higher than the number of CPU +# cores if changing this parameter. + +# The maximum number of connections for Nginx is calculated by: +# max_clients = worker_processes * worker_connections +worker_processes auto; + +# Maximum open file descriptors per process; +# should be > worker_connections. +worker_rlimit_nofile 8192; + +events { + # When you need > 8000 * cpu_cores connections, you start optimizing your OS, + # and this is probably the point at which you hire people who are smarter than + # you, as this is *a lot* of requests. + worker_connections 8000; +} + +# Default error log file +# (this is only used when you don't override error_log on a server{} level) +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx/nginx.pid; + +http { + # Include proxy and server configuration. + include /etc/nginx/proxy.conf; + include /etc/nginx/http.d/bitwarden.conf; + + # Hide nginx version information. + server_tokens off; + + # Define the MIME types for files. + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # Update charset_types to match updated mime.types. + # text/html is always included by charset module. + # Default: text/html text/xml text/plain text/vnd.wap.wml application/javascript application/rss+xml + charset_types + text/css + text/plain + text/vnd.wap.wml + application/javascript + application/json + application/rss+xml + application/xml; + + # Format to use in log files + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + # Default log file + # (this is only used when you don't override access_log on a server{} level) + access_log /var/log/nginx/access.log main; + + # How long to allow each connection to stay idle; longer values are better + # for each individual client, particularly for SSL, but means that worker + # connections are tied up longer. (Default: 65) + keepalive_timeout 20; + + # Speed up file transfers by using sendfile() to copy directly + # between descriptors rather than using read()/write(). + # For performance reasons, on FreeBSD systems w/ ZFS + # this option should be disabled as ZFS's ARC caches + # frequently used files in RAM by default. + sendfile on; + + # Tell Nginx not to send out partial frames; this increases throughput + # since TCP frames are filled up before being sent out. (adds TCP_CORK) + tcp_nopush on; + + + # Compression + + # Enable Gzip compressed. + gzip on; + + # Compression level (1-9). + # 5 is a perfect compromise between size and cpu usage, offering about + # 75% reduction for most ascii files (almost identical to level 9). + gzip_comp_level 5; + + # Don't compress anything that's already small and unlikely to shrink much + # if at all (the default is 20 bytes, which is bad as that usually leads to + # larger files after gzipping). + gzip_min_length 256; + + # Compress data even for clients that are connecting to us via proxies, + # identified by the "Via" header (required for CloudFront). + gzip_proxied any; + + # Tell proxies to cache both the gzipped and regular version of a resource + # whenever the client's Accept-Encoding capabilities header varies; + # Avoids the issue where a non-gzip capable client (which is extremely rare + # today) would display gibberish if their proxy gave them the gzipped version. + gzip_vary on; + + # Compress all output labeled with one of the following MIME-types. + gzip_types + application/atom+xml + application/javascript + application/json + application/ld+json + application/manifest+json + application/rss+xml + application/vnd.geo+json + application/vnd.ms-fontobject + application/x-font-ttf + application/x-web-app-manifest+json + application/xhtml+xml + application/xml + font/opentype + image/bmp + image/svg+xml + image/x-icon + text/cache-manifest + text/css + text/plain + text/vcard + text/vnd.rim.location.xloc + text/vtt + text/x-component + text/x-cross-domain-policy; + # text/html is always compressed by HttpGzipModule + + # This should be turned on if you are going to have pre-compressed copies (.gz) of + # static files available. If not it should be left off as it will cause extra I/O + # for the check. It is best if you enable this in a location{} block for + # a specific directory, or on an individual server{} level. + # gzip_static on; + + # Content type for FIDO U2F facets + map $uri $fido_content_type { + default "application/fido.trusted-apps+json"; + } +} diff --git a/docker-unified/nginx/proxy.conf b/docker-unified/nginx/proxy.conf new file mode 100644 index 000000000..7e7941513 --- /dev/null +++ b/docker-unified/nginx/proxy.conf @@ -0,0 +1,15 @@ +proxy_redirect off; +proxy_set_header Host $host; +proxy_set_header X-Real-IP $remote_addr; +proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +proxy_set_header X-Url-Scheme $scheme; +proxy_set_header X-Forwarded-Proto $scheme; +client_max_body_size 505m; +client_body_buffer_size 128k; +proxy_connect_timeout 90; +proxy_send_timeout 90; +proxy_read_timeout 90; +proxy_buffer_size 128k; +proxy_buffers 4 256k; +proxy_busy_buffers_size 256k; +large_client_header_buffers 4 32k; diff --git a/docker-unified/nginx/security-headers-ssl.conf b/docker-unified/nginx/security-headers-ssl.conf new file mode 100644 index 000000000..d94e835c4 --- /dev/null +++ b/docker-unified/nginx/security-headers-ssl.conf @@ -0,0 +1,2 @@ +# This will enforce HTTP browsing into HTTPS and avoid ssl stripping attack. 6 months age +add_header Strict-Transport-Security max-age=15768000; \ No newline at end of file diff --git a/docker-unified/nginx/security-headers.conf b/docker-unified/nginx/security-headers.conf new file mode 100644 index 000000000..c23d1b497 --- /dev/null +++ b/docker-unified/nginx/security-headers.conf @@ -0,0 +1,3 @@ +add_header Referrer-Policy same-origin; +add_header X-Content-Type-Options nosniff; +add_header X-XSS-Protection "1; mode=block"; \ No newline at end of file diff --git a/docker-unified/settings.env b/docker-unified/settings.env new file mode 100644 index 000000000..a789e7aa4 --- /dev/null +++ b/docker-unified/settings.env @@ -0,0 +1,61 @@ +##################### +# Required Settings # +##################### + +# Server hostname +BW_DOMAIN=bitwarden.yourdomain.com + +# Database +# Available providers are sqlserver, postgresql, or mysql/mariadb +BW_DB_PROVIDER=mysql +BW_DB_SERVER=db +BW_DB_DATABASE=bitwarden_vault +BW_DB_USERNAME=bitwarden +BW_DB_PASSWORD=super_strong_password + +# Installation information +# Get your ID and key from https://bitwarden.com/host/ +BW_INSTALLATION_ID=00000000-0000-0000-0000-000000000000 +BW_INSTALLATION_KEY=xxxxxxxxxxxx + +##################### +# Optional Settings # +##################### +# Learn more here: https://bitwarden.com/help/environment-variables/ + +# SSL +#BW_ENABLE_SSL=true +#BW_ENABLE_SSL_CA=true +#BW_SSL_CERT=ssl.crt +#BW_SSL_KEY=ssl.key +#BW_SSL_CA_CERT=ca.crt + +# Services +# Some services, namely for enterprise use cases, are disabled by default. Defaults shown below. +#BW_ENABLE_ADMIN=true +#BW_ENABLE_API=true +#BW_ENABLE_EVENTS=false +#BW_ENABLE_ICONS=true +#BW_ENABLE_IDENTITY=true +#BW_ENABLE_NOTIFICATIONS=true +#BW_ENABLE_SCIM=false +#BW_ENABLE_SSO=false + +#BW_ICONS_PROXY_TO_CLOUD=false + +# Mail +#globalSettings__mail__replyToEmail=noreply@$BW_DOMAIN +#globalSettings__mail__smtp__host=smtphost.example.com +#globalSettings__mail__smtp__port=587 +#globalSettings__mail__smtp__ssl=false +#globalSettings__mail__smtp__username=smtpusername +#globalSettings__mail__smtp__password=smtppassword + +# Yubikey +#globalSettings__yubico__clientId=REPLACE +#globalSettings__yubico__key=REPLACE + +# Other +#globalSettings__disableUserRegistration=false +#globalSettings__hibpApiKey=REPLACE +#adminSettings__admins="admin1@email.com,admin2@email.com" diff --git a/docker-unified/supervisord/admin.ini b/docker-unified/supervisord/admin.ini new file mode 100644 index 000000000..113da5e99 --- /dev/null +++ b/docker-unified/supervisord/admin.ini @@ -0,0 +1,9 @@ +[program:admin] +autostart=true +autorestart=true +command=/usr/bin/dotnet "Admin.dll" +directory=/app/Admin +environment=ASPNETCORE_URLS="http://+:5000" +redirect_stderr=true +startsecs=15 +stdout_logfile=/var/log/bitwarden/admin.log diff --git a/docker-unified/supervisord/api.ini b/docker-unified/supervisord/api.ini new file mode 100644 index 000000000..410e1d8b8 --- /dev/null +++ b/docker-unified/supervisord/api.ini @@ -0,0 +1,9 @@ +[program:api] +autostart=true +autorestart=true +command=/usr/bin/dotnet "Api.dll" +directory=/app/Api +environment=ASPNETCORE_URLS="http://+:5001" +redirect_stderr=true +startsecs=15 +stdout_logfile=/var/log/bitwarden/api.log diff --git a/docker-unified/supervisord/events.ini b/docker-unified/supervisord/events.ini new file mode 100644 index 000000000..32093d2d1 --- /dev/null +++ b/docker-unified/supervisord/events.ini @@ -0,0 +1,9 @@ +[program:events] +autostart=true +autorestart=true +command=/usr/bin/dotnet "Events.dll" +directory=/app/Events +environment=ASPNETCORE_URLS="http://+:5003" +redirect_stderr=true +startsecs=15 +stdout_logfile=/var/log/bitwarden/events.log diff --git a/docker-unified/supervisord/icons.ini b/docker-unified/supervisord/icons.ini new file mode 100644 index 000000000..36489e8bb --- /dev/null +++ b/docker-unified/supervisord/icons.ini @@ -0,0 +1,9 @@ +[program:icons] +autostart=true +autorestart=true +command=/usr/bin/dotnet "Icons.dll" +directory=/app/Icons +environment=ASPNETCORE_URLS="http://+:5004" +redirect_stderr=true +startsecs=15 +stdout_logfile=/var/log/bitwarden/icons.log diff --git a/docker-unified/supervisord/identity.ini b/docker-unified/supervisord/identity.ini new file mode 100644 index 000000000..4b1600ce9 --- /dev/null +++ b/docker-unified/supervisord/identity.ini @@ -0,0 +1,10 @@ +[program:identity] +autostart=true +autorestart=true +command=/usr/bin/dotnet "Identity.dll" +directory=/app/Identity +environment=ASPNETCORE_URLS="http://+:5005" +priority=1 +redirect_stderr=true +startsecs=15 +stdout_logfile=/var/log/bitwarden/identity.log diff --git a/docker-unified/supervisord/nginx.ini b/docker-unified/supervisord/nginx.ini new file mode 100644 index 000000000..bc52f3f91 --- /dev/null +++ b/docker-unified/supervisord/nginx.ini @@ -0,0 +1,7 @@ +[program:nginx] +autostart=true +autorestart=true +command=nginx +redirect_stderr=true +startsecs=15 +stdout_logfile=/var/log/bitwarden/nginx.log diff --git a/docker-unified/supervisord/notifications.ini b/docker-unified/supervisord/notifications.ini new file mode 100644 index 000000000..2744ff711 --- /dev/null +++ b/docker-unified/supervisord/notifications.ini @@ -0,0 +1,9 @@ +[program:notifications] +autostart=true +autorestart=true +command=/usr/bin/dotnet "Notifications.dll" +directory=/app/Notifications +environment=ASPNETCORE_URLS="http://+:5006" +redirect_stderr=true +startsecs=15 +stdout_logfile=/var/log/bitwarden/notifications.log diff --git a/docker-unified/supervisord/scim.ini b/docker-unified/supervisord/scim.ini new file mode 100644 index 000000000..11d00d4c2 --- /dev/null +++ b/docker-unified/supervisord/scim.ini @@ -0,0 +1,9 @@ +[program:scim] +autostart=true +autorestart=true +command=/usr/bin/dotnet "Scim.dll" +directory=/app/Scim +environment=ASPNETCORE_URLS="http://+:5002" +redirect_stderr=true +startsecs=15 +stdout_logfile=/var/log/bitwarden/scim.log diff --git a/docker-unified/supervisord/sso.ini b/docker-unified/supervisord/sso.ini new file mode 100644 index 000000000..cb29c731a --- /dev/null +++ b/docker-unified/supervisord/sso.ini @@ -0,0 +1,9 @@ +[program:sso] +autostart=true +autorestart=true +command=/usr/bin/dotnet "Sso.dll" +directory=/app/Sso +environment=ASPNETCORE_URLS="http://+:5007" +redirect_stderr=true +startsecs=15 +stdout_logfile=/var/log/bitwarden/sso.log diff --git a/docker-unified/supervisord/supervisord.conf b/docker-unified/supervisord/supervisord.conf new file mode 100644 index 000000000..4cd6a8cb4 --- /dev/null +++ b/docker-unified/supervisord/supervisord.conf @@ -0,0 +1,15 @@ +[unix_http_server] +file=/run/supervisord.sock ; the path to the socket file + +[supervisord] +logfile=/var/log/supervisord.log ; main log file; default $CWD/supervisord.log +nodaemon=true ; start in foreground if true; default false + +[rpcinterface:supervisor] +supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface + +[supervisorctl] +serverurl=unix:///run/supervisord.sock ; use a unix:// URL for a unix socket + +[include] +files = /etc/supervisor.d/*.ini diff --git a/scripts/build b/scripts/build index c8b5cdff0..38b457f85 100755 --- a/scripts/build +++ b/scripts/build @@ -22,27 +22,26 @@ build() { PROJECT=$1; shift case "$PROJECT" in - "api" | "Api") build Api $PWD/src/Api ;; "admin" | "Admin") build Admin $PWD/src/Admin ;; - "identity" | "Identity") build Identity $PWD/src/Identity ;; - "events" | "Events") build Events $PWD/src/Events ;; + "api" | "Api") build Api $PWD/src/Api ;; "billing" | "Billing") build Billing $PWD/src/Billing ;; - "sso" | "Sso") build Sso $PWD/bitwarden_license/src/Sso ;; - "server" | "Server") build Server $PWD/util/Server ;; - "icons" | "Icons") build Icons $PWD/src/Icons ;; - "notifications" | "Notifications") build Notifications $PWD/src/Notifications ;; - "setup" | "Setup") build Setup $PWD/util/Setup ;; + "events" | "Events") build Events $PWD/src/Events ;; "eventsprocessor" | "EventsProcessor") build EventsProcessor $PWD/src/EventsProcessor ;; + "icons" | "Icons") build Icons $PWD/src/Icons ;; + "identity" | "Identity") build Identity $PWD/src/Identity ;; + "notifications" | "Notifications") build Notifications $PWD/src/Notifications ;; + "server" | "Server") build Server $PWD/util/Server ;; + "sso" | "Sso") build Sso $PWD/bitwarden_license/src/Sso ;; "") - build Api $PWD/src/Api - build Admin $PWD/src/Admin - build Identity $PWD/src/Identity - build Events $PWD/src/Events - build Billing $PWD/src/Billing - build Sso $PWD/bitwarden_license/src/Sso - build Server $PWD/util/Server - build Icons $PWD/src/Icons - build Notifications $PWD/src/Notifications + build Admin $PWD/src/Admin + build Api $PWD/src/Api + build Billing $PWD/src/Billing + build Events $PWD/src/Events build EventsProcessor $PWD/src/EventsProcessor + build Icons $PWD/src/Icons + build Identity $PWD/src/Identity + build Notifications $PWD/src/Notifications + build Server $PWD/util/Server + build Sso $PWD/bitwarden_license/src/Sso ;; esac diff --git a/scripts/build-docker b/scripts/build-docker index bb9e6d350..b1c643319 100755 --- a/scripts/build-docker +++ b/scripts/build-docker @@ -22,10 +22,6 @@ docker_build() { echo "Building docker image: bitwarden/$project_name_lower:$docker_tag" echo "==============================" - if [ "$project_name_lower" == "k8s-proxy" ]; then - docker build -f $project_dir/Dockerfile-k8s -t bitwarden/$project_name_lower:$docker_tag $project_dir - fi - docker build -t bitwarden/$project_name_lower:$docker_tag $project_dir if [ "$docker_push" == "1" ]; then @@ -59,35 +55,34 @@ done case "$PROJECT" in - "api" | "Api") docker_build Api $PWD/src/Api $TAG $PUSH ;; "admin" | "Admin") docker_build Admin $PWD/src/Admin $TAG $PUSH ;; - "identity" | "Identity") docker_build Identity $PWD/src/Identity $TAG $PUSH ;; - "events" | "Events") docker_build Events $PWD/src/Events $TAG $PUSH ;; - #"billing" | "Billing") docker_build Billing $PWD/src/Billing $TAG $PUSH ;; - "sso" | "Sso") docker_build Sso $PWD/bitwarden_license/src/Sso $TAG $PUSH ;; - "server" | "Server") docker_build Server $PWD/util/Server $TAG $PUSH ;; - "nginx" | "Nginx") docker_build Nginx $PWD/util/Nginx $TAG $PUSH ;; - "k8s-proxy" | "K8s-Proxy") docker_build K8s-Proxy $PWD/util/Nginx $TAG $PUSH ;; + "api" | "Api") docker_build Api $PWD/src/Api $TAG $PUSH ;; "attachments" | "Attachments") docker_build Attachments $PWD/util/Attachments $TAG $PUSH ;; - "icons" | "Icons") docker_build Icons $PWD/src/Icons $TAG $PUSH ;; - "notifications" | "Notifications") docker_build Notifications $PWD/src/Notifications $TAG $PUSH ;; - "mssql" | "MsSql" | "Mssql") docker_build MsSql $PWD/util/MsSql $TAG $PUSH ;; - "setup" | "Setup") docker_build Setup $PWD/util/Setup $TAG $PUSH ;; + #"billing" | "Billing") docker_build Billing $PWD/src/Billing $TAG $PUSH ;; + "events" | "Events") docker_build Events $PWD/src/Events $TAG $PUSH ;; "eventsprocessor" | "EventsProcessor") docker_build EventsProcessor $PWD/src/EventsProcessor $TAG $PUSH ;; + "icons" | "Icons") docker_build Icons $PWD/src/Icons $TAG $PUSH ;; + "identity" | "Identity") docker_build Identity $PWD/src/Identity $TAG $PUSH ;; + "mssql" | "MsSql" | "Mssql") docker_build MsSql $PWD/util/MsSql $TAG $PUSH ;; + "nginx" | "Nginx") docker_build Nginx $PWD/util/Nginx $TAG $PUSH ;; + "notifications" | "Notifications") docker_build Notifications $PWD/src/Notifications $TAG $PUSH ;; + "server" | "Server") docker_build Server $PWD/util/Server $TAG $PUSH ;; + "setup" | "Setup") docker_build Setup $PWD/util/Setup $TAG $PUSH ;; + "sso" | "Sso") docker_build Sso $PWD/bitwarden_license/src/Sso $TAG $PUSH ;; "") - docker_build Api $PWD/src/Api $TAG $PUSH docker_build Admin $PWD/src/Admin $TAG $PUSH - docker_build Identity $PWD/src/Identity $TAG $PUSH - docker_build Events $PWD/src/Events $TAG $PUSH - #docker_build Billing $PWD/src/Billing $TAG $PUSH - docker_build Sso $PWD/bitwarden_license/src/Sso $TAG $PUSH - docker_build Server $PWD/util/Server $TAG $PUSH - docker_build Nginx $PWD/util/Nginx $TAG $PUSH + docker_build Api $PWD/src/Api $TAG $PUSH docker_build Attachments $PWD/util/Attachments $TAG $PUSH - docker_build Icons $PWD/src/Icons $TAG $PUSH - docker_build Notifications $PWD/src/Notifications $TAG $PUSH - docker_build MsSql $PWD/util/MsSql $TAG $PUSH - docker_build Setup $PWD/util/Setup $TAG $PUSH + #docker_build Billing $PWD/src/Billing $TAG $PUSH + docker_build Events $PWD/src/Events $TAG $PUSH docker_build EventsProcessor $PWD/src/EventsProcessor $TAG $PUSH + docker_build Icons $PWD/src/Icons $TAG $PUSH + docker_build Identity $PWD/src/Identity $TAG $PUSH + docker_build MsSql $PWD/util/MsSql $TAG $PUSH + docker_build Nginx $PWD/util/Nginx $TAG $PUSH + docker_build Notifications $PWD/src/Notifications $TAG $PUSH + docker_build Server $PWD/util/Server $TAG $PUSH + docker_build Setup $PWD/util/Setup $TAG $PUSH + docker_build Sso $PWD/bitwarden_license/src/Sso $TAG $PUSH ;; esac diff --git a/src/Admin/Admin.csproj b/src/Admin/Admin.csproj index fa3d2f452..adf6077a2 100644 --- a/src/Admin/Admin.csproj +++ b/src/Admin/Admin.csproj @@ -5,6 +5,8 @@ + + diff --git a/src/Admin/HostedServices/DatabaseMigrationHostedService.cs b/src/Admin/HostedServices/DatabaseMigrationHostedService.cs index 0f660729e..d133d3963 100644 --- a/src/Admin/HostedServices/DatabaseMigrationHostedService.cs +++ b/src/Admin/HostedServices/DatabaseMigrationHostedService.cs @@ -1,25 +1,19 @@ using System.Data.SqlClient; -using Bit.Core.Jobs; -using Bit.Core.Settings; -using Bit.Migrator; +using Bit.Core.Utilities; namespace Bit.Admin.HostedServices; public class DatabaseMigrationHostedService : IHostedService, IDisposable { - private readonly GlobalSettings _globalSettings; private readonly ILogger _logger; - private readonly DbMigrator _dbMigrator; + private readonly IDbMigrator _dbMigrator; public DatabaseMigrationHostedService( - GlobalSettings globalSettings, - ILogger logger, - ILogger migratorLogger, - ILogger listenerLogger) + IDbMigrator dbMigrator, + ILogger logger) { - _globalSettings = globalSettings; _logger = logger; - _dbMigrator = new DbMigrator(globalSettings.SqlServer.ConnectionString, migratorLogger); + _dbMigrator = dbMigrator; } public virtual async Task StartAsync(CancellationToken cancellationToken) @@ -32,7 +26,7 @@ public class DatabaseMigrationHostedService : IHostedService, IDisposable { try { - _dbMigrator.MigrateMsSqlDatabase(true, cancellationToken); + _dbMigrator.MigrateDatabase(true, cancellationToken); // TODO: Maybe flip a flag somewhere to indicate migration is complete?? break; } diff --git a/src/Admin/Startup.cs b/src/Admin/Startup.cs index d10a1d445..63e4fd950 100644 --- a/src/Admin/Startup.cs +++ b/src/Admin/Startup.cs @@ -42,7 +42,21 @@ public class Startup StripeConfiguration.MaxNetworkRetries = globalSettings.Stripe.MaxNetworkRetries; // Repositories - services.AddSqlServerRepositories(globalSettings); + var databaseProvider = services.AddDatabaseRepositories(globalSettings); + switch (databaseProvider) + { + case Core.Enums.SupportedDatabaseProviders.SqlServer: + services.AddSingleton(); + break; + case Core.Enums.SupportedDatabaseProviders.MySql: + services.AddSingleton(); + break; + case Core.Enums.SupportedDatabaseProviders.Postgres: + services.AddSingleton(); + break; + default: + break; + } // Context services.AddScoped(); diff --git a/src/Admin/packages.lock.json b/src/Admin/packages.lock.json index b59a3eacd..372d7a2d5 100644 --- a/src/Admin/packages.lock.json +++ b/src/Admin/packages.lock.json @@ -3211,82 +3211,96 @@ "commercial.core": { "type": "Project", "dependencies": { - "Core": "2022.8.4" + "Core": "[2022.10.0, )" } }, "core": { "type": "Project", "dependencies": { - "AWSSDK.SQS": "3.7.2.47", - "AWSSDK.SimpleEmail": "3.7.0.150", - "AspNetCoreRateLimit": "4.0.2", - "AspNetCoreRateLimit.Redis": "1.0.1", - "Azure.Extensions.AspNetCore.DataProtection.Blobs": "1.2.1", - "Azure.Storage.Blobs": "12.11.0", - "Azure.Storage.Queues": "12.9.0", - "BitPay.Light": "1.0.1907", - "Braintree": "5.12.0", - "Fido2.AspNet": "3.0.0-beta2", - "Handlebars.Net": "2.1.2", - "IdentityServer4": "4.1.2", - "IdentityServer4.AccessTokenValidation": "3.0.1", - "MailKit": "3.2.0", - "Microsoft.AspNetCore.Authentication.JwtBearer": "6.0.4", - "Microsoft.Azure.Cosmos.Table": "1.0.8", - "Microsoft.Azure.NotificationHubs": "4.1.0", - "Microsoft.Azure.ServiceBus": "5.2.0", - "Microsoft.Data.SqlClient": "4.1.0", - "Microsoft.Extensions.Caching.StackExchangeRedis": "6.0.6", - "Microsoft.Extensions.Configuration.EnvironmentVariables": "6.0.1", - "Microsoft.Extensions.Configuration.UserSecrets": "6.0.1", - "Microsoft.Extensions.Identity.Stores": "6.0.4", - "Newtonsoft.Json": "13.0.1", - "Otp.NET": "1.2.2", - "Quartz": "3.4.0", - "SendGrid": "9.27.0", - "Sentry.Serilog": "3.16.0", - "Serilog.AspNetCore": "5.0.0", - "Serilog.Extensions.Logging": "3.1.0", - "Serilog.Extensions.Logging.File": "2.0.0", - "Serilog.Sinks.AzureCosmosDB": "2.0.0", - "Serilog.Sinks.SyslogMessages": "2.0.6", - "Stripe.net": "40.0.0", - "YubicoDotNetClient": "1.2.0" + "AWSSDK.SQS": "[3.7.2.47, )", + "AWSSDK.SimpleEmail": "[3.7.0.150, )", + "AspNetCoreRateLimit": "[4.0.2, )", + "AspNetCoreRateLimit.Redis": "[1.0.1, )", + "Azure.Extensions.AspNetCore.DataProtection.Blobs": "[1.2.1, )", + "Azure.Storage.Blobs": "[12.11.0, )", + "Azure.Storage.Queues": "[12.9.0, )", + "BitPay.Light": "[1.0.1907, )", + "Braintree": "[5.12.0, )", + "Fido2.AspNet": "[3.0.0-beta2, )", + "Handlebars.Net": "[2.1.2, )", + "IdentityServer4": "[4.1.2, )", + "IdentityServer4.AccessTokenValidation": "[3.0.1, )", + "MailKit": "[3.2.0, )", + "Microsoft.AspNetCore.Authentication.JwtBearer": "[6.0.4, )", + "Microsoft.Azure.Cosmos.Table": "[1.0.8, )", + "Microsoft.Azure.NotificationHubs": "[4.1.0, )", + "Microsoft.Azure.ServiceBus": "[5.2.0, )", + "Microsoft.Data.SqlClient": "[4.1.0, )", + "Microsoft.Extensions.Caching.StackExchangeRedis": "[6.0.6, )", + "Microsoft.Extensions.Configuration.EnvironmentVariables": "[6.0.1, )", + "Microsoft.Extensions.Configuration.UserSecrets": "[6.0.1, )", + "Microsoft.Extensions.Identity.Stores": "[6.0.4, )", + "Newtonsoft.Json": "[13.0.1, )", + "Otp.NET": "[1.2.2, )", + "Quartz": "[3.4.0, )", + "SendGrid": "[9.27.0, )", + "Sentry.Serilog": "[3.16.0, )", + "Serilog.AspNetCore": "[5.0.0, )", + "Serilog.Extensions.Logging": "[3.1.0, )", + "Serilog.Extensions.Logging.File": "[2.0.0, )", + "Serilog.Sinks.AzureCosmosDB": "[2.0.0, )", + "Serilog.Sinks.SyslogMessages": "[2.0.6, )", + "Stripe.net": "[40.0.0, )", + "YubicoDotNetClient": "[1.2.0, )" } }, "infrastructure.dapper": { "type": "Project", "dependencies": { - "Core": "2022.8.4", - "Dapper": "2.0.123", - "System.Data.SqlClient": "4.8.3" + "Core": "[2022.10.0, )", + "Dapper": "[2.0.123, )", + "System.Data.SqlClient": "[4.8.3, )" } }, "infrastructure.entityframework": { "type": "Project", "dependencies": { - "AutoMapper.Extensions.Microsoft.DependencyInjection": "11.0.0", - "Core": "2022.8.4", - "Microsoft.EntityFrameworkCore.Relational": "6.0.4", - "Npgsql.EntityFrameworkCore.PostgreSQL": "6.0.4", - "Pomelo.EntityFrameworkCore.MySql": "6.0.1", - "linq2db.EntityFrameworkCore": "6.7.1" + "AutoMapper.Extensions.Microsoft.DependencyInjection": "[11.0.0, )", + "Core": "[2022.10.0, )", + "Microsoft.EntityFrameworkCore.Relational": "[6.0.4, )", + "Npgsql.EntityFrameworkCore.PostgreSQL": "[6.0.4, )", + "Pomelo.EntityFrameworkCore.MySql": "[6.0.1, )", + "linq2db.EntityFrameworkCore": "[6.7.1, )" } }, "migrator": { "type": "Project", "dependencies": { - "Core": "2022.8.4", - "Microsoft.Extensions.Logging": "6.0.0", - "dbup-sqlserver": "4.5.0" + "Core": "[2022.10.0, )", + "Microsoft.Extensions.Logging": "[6.0.0, )", + "dbup-sqlserver": "[4.5.0, )" + } + }, + "mysqlmigrations": { + "type": "Project", + "dependencies": { + "Core": "[2022.10.0, )", + "Infrastructure.EntityFramework": "[2022.10.0, )" + } + }, + "postgresmigrations": { + "type": "Project", + "dependencies": { + "Core": "[2022.10.0, )", + "Infrastructure.EntityFramework": "[2022.10.0, )" } }, "sharedweb": { "type": "Project", "dependencies": { - "Core": "2022.8.4", - "Infrastructure.Dapper": "2022.8.4", - "Infrastructure.EntityFramework": "2022.8.4" + "Core": "[2022.10.0, )", + "Infrastructure.Dapper": "[2022.10.0, )", + "Infrastructure.EntityFramework": "[2022.10.0, )" } } } diff --git a/src/Api/Startup.cs b/src/Api/Startup.cs index 20b707f5d..86d130e1e 100644 --- a/src/Api/Startup.cs +++ b/src/Api/Startup.cs @@ -58,7 +58,7 @@ public class Startup StripeConfiguration.MaxNetworkRetries = globalSettings.Stripe.MaxNetworkRetries; // Repositories - services.AddSqlServerRepositories(globalSettings); + services.AddDatabaseRepositories(globalSettings); // Context services.AddScoped(); diff --git a/src/Billing/Startup.cs b/src/Billing/Startup.cs index 328e6133d..2ad7d0ce7 100644 --- a/src/Billing/Startup.cs +++ b/src/Billing/Startup.cs @@ -34,7 +34,7 @@ public class Startup StripeConfiguration.MaxNetworkRetries = globalSettings.Stripe.MaxNetworkRetries; // Repositories - services.AddSqlServerRepositories(globalSettings); + services.AddDatabaseRepositories(globalSettings); // PayPal Client services.AddSingleton(); diff --git a/src/Core/Settings/GlobalSettings.cs b/src/Core/Settings/GlobalSettings.cs index 8c762296d..9fe5e99bd 100644 --- a/src/Core/Settings/GlobalSettings.cs +++ b/src/Core/Settings/GlobalSettings.cs @@ -24,6 +24,7 @@ public class GlobalSettings : IGlobalSettings get => BuildDirectory(_logDirectory, "/logs"); set => _logDirectory = value; } + public virtual bool LogDirectoryByProject { get; set; } = true; public virtual long? LogRollBySizeLimit { get; set; } public virtual bool EnableDevLogging { get; set; } = false; public virtual string LicenseDirectory diff --git a/src/Core/Utilities/IDbMigrator.cs b/src/Core/Utilities/IDbMigrator.cs new file mode 100644 index 000000000..c2a8886f1 --- /dev/null +++ b/src/Core/Utilities/IDbMigrator.cs @@ -0,0 +1,7 @@ +namespace Bit.Core.Utilities; + +public interface IDbMigrator +{ + bool MigrateDatabase(bool enableLogging = true, + CancellationToken cancellationToken = default(CancellationToken)); +} diff --git a/src/Core/Utilities/LoggerFactoryExtensions.cs b/src/Core/Utilities/LoggerFactoryExtensions.cs index f6ea43882..145bf5f6c 100644 --- a/src/Core/Utilities/LoggerFactoryExtensions.cs +++ b/src/Core/Utilities/LoggerFactoryExtensions.cs @@ -125,13 +125,22 @@ public static class LoggerFactoryExtensions { if (globalSettings.LogRollBySizeLimit.HasValue) { - config.WriteTo.File($"{globalSettings.LogDirectory}/{globalSettings.ProjectName}/log.txt", - rollOnFileSizeLimit: true, fileSizeLimitBytes: globalSettings.LogRollBySizeLimit); + var pathFormat = Path.Combine(globalSettings.LogDirectory, $"{globalSettings.ProjectName.ToLowerInvariant()}.log"); + if (globalSettings.LogDirectoryByProject) + { + pathFormat = Path.Combine(globalSettings.LogDirectory, globalSettings.ProjectName, "log.txt"); + } + config.WriteTo.File(pathFormat, rollOnFileSizeLimit: true, + fileSizeLimitBytes: globalSettings.LogRollBySizeLimit); } else { - config.WriteTo - .RollingFile($"{globalSettings.LogDirectory}/{globalSettings.ProjectName}/{{Date}}.txt"); + var pathFormat = Path.Combine(globalSettings.LogDirectory, $"{globalSettings.ProjectName.ToLowerInvariant()}_{{Date}}.log"); + if (globalSettings.LogDirectoryByProject) + { + pathFormat = Path.Combine(globalSettings.LogDirectory, globalSettings.ProjectName, "{Date}.txt"); + } + config.WriteTo.RollingFile(pathFormat); } config .Enrich.FromLogContext() diff --git a/src/Events/Startup.cs b/src/Events/Startup.cs index c44ca3c1a..197cbdf18 100644 --- a/src/Events/Startup.cs +++ b/src/Events/Startup.cs @@ -29,7 +29,7 @@ public class Startup var globalSettings = services.AddGlobalSettingsServices(Configuration, Environment); // Repositories - services.AddSqlServerRepositories(globalSettings); + services.AddDatabaseRepositories(globalSettings); // Context services.AddScoped(); diff --git a/src/Identity/Startup.cs b/src/Identity/Startup.cs index 170e2b931..0dcec6ad4 100644 --- a/src/Identity/Startup.cs +++ b/src/Identity/Startup.cs @@ -44,7 +44,7 @@ public class Startup services.AddCustomDataProtectionServices(Environment, globalSettings); // Repositories - services.AddSqlServerRepositories(globalSettings); + services.AddDatabaseRepositories(globalSettings); // Context services.AddScoped(); diff --git a/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs b/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs index 06897f139..41065fee8 100644 --- a/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs +++ b/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs @@ -22,13 +22,14 @@ public static class EntityFrameworkServiceCollectionExtensions { if (provider == SupportedDatabaseProviders.Postgres) { - options.UseNpgsql(connectionString); + options.UseNpgsql(connectionString, b => b.MigrationsAssembly("PostgresMigrations")); // Handle NpgSql Legacy Support for `timestamp without timezone` issue AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true); } else if (provider == SupportedDatabaseProviders.MySql) { - options.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString)); + options.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString), + b => b.MigrationsAssembly("MySqlMigrations")); } }); services.AddSingleton(); diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index fa6a58892..f5f188daa 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -47,7 +47,7 @@ namespace Bit.SharedWeb.Utilities; public static class ServiceCollectionExtensions { - public static void AddSqlServerRepositories(this IServiceCollection services, GlobalSettings globalSettings) + public static SupportedDatabaseProviders AddDatabaseRepositories(this IServiceCollection services, GlobalSettings globalSettings) { var selectedDatabaseProvider = globalSettings.DatabaseProvider; var provider = SupportedDatabaseProviders.SqlServer; @@ -93,6 +93,8 @@ public static class ServiceCollectionExtensions services.AddSingleton(); services.AddSingleton(); } + + return provider; } public static void AddBaseServices(this IServiceCollection services, IGlobalSettings globalSettings) diff --git a/util/EfShared/MigrationBuilderExtensions.cs b/util/EfShared/MigrationBuilderExtensions.cs index cb9fad33c..b59ad207f 100644 --- a/util/EfShared/MigrationBuilderExtensions.cs +++ b/util/EfShared/MigrationBuilderExtensions.cs @@ -1,13 +1,11 @@ - - -using System.Runtime.CompilerServices; +using System.Runtime.CompilerServices; using Bit.Core.Utilities; using Microsoft.EntityFrameworkCore.Migrations; -namespace Bit; +namespace Bit.EfShared; // This file is a manual addition to a project that it helps, a project that chooses to compile it -// should have a projet reference to Core.csproj and a package reference to Microsoft.EntityFrameworkCore.Design +// should have a project reference to Core.csproj and a package reference to Microsoft.EntityFrameworkCore.Design // The reason for this is that if it belonged to it's own library you would have to add manual references to the above // and manage the version for the EntityFrameworkCore package. This way it also doesn't create another dll // To include this you can view examples in the MySqlMigrations and PostgresMigrations .csproj files. diff --git a/util/Migrator/SqlServerDbMigrator.cs b/util/Migrator/SqlServerDbMigrator.cs new file mode 100644 index 000000000..374e29e74 --- /dev/null +++ b/util/Migrator/SqlServerDbMigrator.cs @@ -0,0 +1,109 @@ +using System.Data; +using System.Data.SqlClient; +using System.Reflection; +using Bit.Core; +using Bit.Core.Settings; +using Bit.Core.Utilities; +using DbUp; +using Microsoft.Extensions.Logging; + +namespace Bit.Migrator; + +public class SqlServerDbMigrator : IDbMigrator +{ + private readonly string _connectionString; + private readonly ILogger _logger; + private readonly string _masterConnectionString; + + public SqlServerDbMigrator(GlobalSettings globalSettings, ILogger logger) + { + _connectionString = globalSettings.SqlServer.ConnectionString; + _logger = logger; + _masterConnectionString = new SqlConnectionStringBuilder(_connectionString) + { + InitialCatalog = "master" + }.ConnectionString; + } + + public bool MigrateDatabase(bool enableLogging = true, + CancellationToken cancellationToken = default(CancellationToken)) + { + if (enableLogging && _logger != null) + { + _logger.LogInformation(Constants.BypassFiltersEventId, "Migrating database."); + } + + using (var connection = new SqlConnection(_masterConnectionString)) + { + var databaseName = new SqlConnectionStringBuilder(_connectionString).InitialCatalog; + if (string.IsNullOrWhiteSpace(databaseName)) + { + databaseName = "vault"; + } + + var databaseNameQuoted = new SqlCommandBuilder().QuoteIdentifier(databaseName); + var command = new SqlCommand( + "IF ((SELECT COUNT(1) FROM sys.databases WHERE [name] = @DatabaseName) = 0) " + + "CREATE DATABASE " + databaseNameQuoted + ";", connection); + command.Parameters.Add("@DatabaseName", SqlDbType.VarChar).Value = databaseName; + command.Connection.Open(); + command.ExecuteNonQuery(); + + command.CommandText = "IF ((SELECT DATABASEPROPERTYEX([name], 'IsAutoClose') " + + "FROM sys.databases WHERE [name] = @DatabaseName) = 1) " + + "ALTER DATABASE " + databaseNameQuoted + " SET AUTO_CLOSE OFF;"; + command.ExecuteNonQuery(); + } + + cancellationToken.ThrowIfCancellationRequested(); + using (var connection = new SqlConnection(_connectionString)) + { + // Rename old migration scripts to new namespace. + var command = new SqlCommand( + "IF OBJECT_ID('Migration','U') IS NOT NULL " + + "UPDATE [dbo].[Migration] SET " + + "[ScriptName] = REPLACE([ScriptName], 'Bit.Setup.', 'Bit.Migrator.');", connection); + command.Connection.Open(); + command.ExecuteNonQuery(); + } + + cancellationToken.ThrowIfCancellationRequested(); + var builder = DeployChanges.To + .SqlDatabase(_connectionString) + .JournalToSqlTable("dbo", "Migration") + .WithScriptsAndCodeEmbeddedInAssembly(Assembly.GetExecutingAssembly(), + s => s.Contains($".DbScripts.") && !s.Contains(".Archive.")) + .WithTransaction() + .WithExecutionTimeout(TimeSpan.FromMinutes(5)); + + if (enableLogging) + { + if (_logger != null) + { + builder.LogTo(new DbUpLogger(_logger)); + } + else + { + builder.LogToConsole(); + } + } + + var upgrader = builder.Build(); + var result = upgrader.PerformUpgrade(); + + if (enableLogging && _logger != null) + { + if (result.Successful) + { + _logger.LogInformation(Constants.BypassFiltersEventId, "Migration successful."); + } + else + { + _logger.LogError(Constants.BypassFiltersEventId, result.Error, "Migration failed."); + } + } + + cancellationToken.ThrowIfCancellationRequested(); + return result.Successful; + } +} diff --git a/util/Migrator/packages.lock.json b/util/Migrator/packages.lock.json index 22384c5da..26e8c5899 100644 --- a/util/Migrator/packages.lock.json +++ b/util/Migrator/packages.lock.json @@ -46,6 +46,23 @@ "StackExchange.Redis": "2.5.43" } }, + "AutoMapper": { + "type": "Transitive", + "resolved": "11.0.0", + "contentHash": "+596AnKykYCk9RxXCEF4GYuapSebQtFVvIA1oVG1rrRkCLAC7AkWehJ0brCfYUbdDW3v1H/p0W3hob7JoXGjMw==", + "dependencies": { + "Microsoft.CSharp": "4.7.0" + } + }, + "AutoMapper.Extensions.Microsoft.DependencyInjection": { + "type": "Transitive", + "resolved": "11.0.0", + "contentHash": "0asw5WxdCFh2OTi9Gv+oKyH9SzxwYQSnO8TV5Dd0GggovILzJW4UimP26JAcxc3yB5NnC5urooZ1BBs8ElpiBw==", + "dependencies": { + "AutoMapper": "11.0.0", + "Microsoft.Extensions.Options": "6.0.0" + } + }, "AWSSDK.Core": { "type": "Transitive", "resolved": "3.7.10.11", @@ -246,6 +263,23 @@ "Microsoft.NETCore.Platforms": "1.0.1" } }, + "linq2db": { + "type": "Transitive", + "resolved": "3.7.0", + "contentHash": "iDous2TbSchtALnTLNXQnprmNZF4GrXas0MBz6ZHWkSdilSJjcf26qFM7Qf98Mny0OXHEmNXG/jtIDhoVJ5KmQ==", + "dependencies": { + "System.ComponentModel.Annotations": "4.7.0" + } + }, + "linq2db.EntityFrameworkCore": { + "type": "Transitive", + "resolved": "6.7.1", + "contentHash": "Bb25vUDyFw3nKnf7KY+bauwKGD0hdM7/syodS+IgHdWlcbH9g7tHxYmMa9+DNuL0yy6DFvP6Q3BkClm7zbQdAw==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Relational": "6.0.0", + "linq2db": "3.7.0" + } + }, "MailKit": { "type": "Transitive", "resolved": "3.2.0", @@ -474,6 +508,39 @@ "resolved": "4.0.0", "contentHash": "wtLlRwQX7YoBUYm25xBjJ3UsuLgycme1xXqDn8t3S5kPCWiZrx8uOkyZHLKzH4kkCiQ9m2/J5JeCKNRbZNn3Qg==" }, + "Microsoft.EntityFrameworkCore": { + "type": "Transitive", + "resolved": "6.0.4", + "contentHash": "gTh3SJsF5WNjEmG32kYc3U4tjeTIv55QOrwHAJcF/xtrIVMteDHMArGC35N0dw86WFY0v8yFkKYKOIOln4jkfQ==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Abstractions": "6.0.4", + "Microsoft.EntityFrameworkCore.Analyzers": "6.0.4", + "Microsoft.Extensions.Caching.Memory": "6.0.1", + "Microsoft.Extensions.DependencyInjection": "6.0.0", + "Microsoft.Extensions.Logging": "6.0.0", + "System.Collections.Immutable": "6.0.0", + "System.Diagnostics.DiagnosticSource": "6.0.0" + } + }, + "Microsoft.EntityFrameworkCore.Abstractions": { + "type": "Transitive", + "resolved": "6.0.4", + "contentHash": "jycTQF0FUJp10cGWBmtsyFhQNeISU9CltDRKCaNiX4QRSEFzgRgaFN4vAFK0T+G5etmXugyddijE4NWCGtgznQ==" + }, + "Microsoft.EntityFrameworkCore.Analyzers": { + "type": "Transitive", + "resolved": "6.0.4", + "contentHash": "t12WodVyGGP2CuLo7R1qwcawHY5zlg+GiQzvkceZpsjcFJVyTFFBFDPg1isBtzurLzWsl+G3z5fVXeic90mPxg==" + }, + "Microsoft.EntityFrameworkCore.Relational": { + "type": "Transitive", + "resolved": "6.0.4", + "contentHash": "E867NbEXYRTElBF5ff+1AN5Awa1jkORy/Rrm0ueibaTAV5uw89LsLoH6yTe+b9urZTWMHtLfGd1RDdNjk8+KzA==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "6.0.4", + "Microsoft.Extensions.Configuration.Abstractions": "6.0.0" + } + }, "Microsoft.Extensions.Caching.Abstractions": { "type": "Transitive", "resolved": "6.0.0", @@ -484,13 +551,14 @@ }, "Microsoft.Extensions.Caching.Memory": { "type": "Transitive", - "resolved": "3.1.8", - "contentHash": "u04q7+tgc8l6pQ5HOcr6scgapkQQHnrhpGoCaaAZd24R36/NxGsGxuhSmhHOrQx9CsBLe2CVBN/4CkLlxtnnXw==", + "resolved": "6.0.1", + "contentHash": "B4y+Cev05eMcjf1na0v9gza6GUtahXbtY1JCypIgx3B4Ea/KAgsWyXEmW4q6zMbmTMtKzmPVk09rvFJirvMwTg==", "dependencies": { - "Microsoft.Extensions.Caching.Abstractions": "3.1.8", - "Microsoft.Extensions.DependencyInjection.Abstractions": "3.1.8", - "Microsoft.Extensions.Logging.Abstractions": "3.1.8", - "Microsoft.Extensions.Options": "3.1.8" + "Microsoft.Extensions.Caching.Abstractions": "6.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", + "Microsoft.Extensions.Logging.Abstractions": "6.0.0", + "Microsoft.Extensions.Options": "6.0.0", + "Microsoft.Extensions.Primitives": "6.0.0" } }, "Microsoft.Extensions.Caching.StackExchangeRedis": { @@ -811,6 +879,11 @@ "System.Security.Cryptography.Pkcs": "6.0.0" } }, + "MySqlConnector": { + "type": "Transitive", + "resolved": "2.1.2", + "contentHash": "JVokQTUNN3WHAu9Vw8ieeq1dXTFokJiig5P0VJ4f439UxRrsPo6SaVWC8Zdm6mkPeQFhZ0/9afdWa02EY/1j/w==" + }, "NETStandard.Library": { "type": "Transitive", "resolved": "1.6.1", @@ -867,6 +940,25 @@ "resolved": "13.0.1", "contentHash": "ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A==" }, + "Npgsql": { + "type": "Transitive", + "resolved": "6.0.4", + "contentHash": "SJMlOmFHr32oOzVXeHmarGaBKkhi0wHVN/rzuu2tUSJ4Qx2AkHCpr9R/DhLWwDiklqgzFU++9wkFyGJxbx/zzg==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "Npgsql.EntityFrameworkCore.PostgreSQL": { + "type": "Transitive", + "resolved": "6.0.4", + "contentHash": "fzgRmBd3nAFvKt/L70sJfFWAdobtwDEeOzOzruJq9og97O8/5B96inQOAgOpYyaUjPYpS4ZS5/bxm3vnOJ0+pQ==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "6.0.4", + "Microsoft.EntityFrameworkCore.Abstractions": "6.0.4", + "Microsoft.EntityFrameworkCore.Relational": "6.0.4", + "Npgsql": "6.0.4" + } + }, "NSec.Cryptography": { "type": "Transitive", "resolved": "20.2.0", @@ -889,6 +981,16 @@ "System.IO.Pipelines": "5.0.1" } }, + "Pomelo.EntityFrameworkCore.MySql": { + "type": "Transitive", + "resolved": "6.0.1", + "contentHash": "sFIo5e9RmQoCTEvH6EeSV8ptmX3dw/6XgyD8R93X/i7A9+XCeG9KTjSNjrszVjVOtCu/eyvYqqcv2uZ/BHhlYA==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Relational": "[6.0.1, 7.0.0)", + "Microsoft.Extensions.DependencyInjection": "6.0.0", + "MySqlConnector": "2.1.2" + } + }, "Portable.BouncyCastle": { "type": "Transitive", "resolved": "1.9.0", @@ -1281,8 +1383,11 @@ }, "System.Collections.Immutable": { "type": "Transitive", - "resolved": "1.7.0", - "contentHash": "RVSM6wZUo6L2y6P3vN6gjUtyJ2IF2RVtrepF3J7nrDKfFQd5u/SnSUFclchYQis8/k5scHy9E+fVeKVQLnnkzw==" + "resolved": "6.0.0", + "contentHash": "l4zZJ1WU2hqpQQHXz1rvC3etVZN+2DLmQMO79FhOTZHMn8tDRr+WU287sbomD0BETlmKDn0ygUgVy9k5xkkJdA==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } }, "System.Collections.NonGeneric": { "type": "Transitive", @@ -1311,6 +1416,11 @@ "System.Threading": "4.0.11" } }, + "System.ComponentModel.Annotations": { + "type": "Transitive", + "resolved": "4.7.0", + "contentHash": "0YFqjhp/mYkDGpU0Ye1GjE53HMp9UVfGN7seGpAMttAC0C40v5gw598jCgpbBLMmCo0E5YRLBv5Z2doypO49ZQ==" + }, "System.Configuration.ConfigurationManager": { "type": "Transitive", "resolved": "6.0.0", @@ -2579,41 +2689,52 @@ "core": { "type": "Project", "dependencies": { - "AWSSDK.SQS": "3.7.2.47", - "AWSSDK.SimpleEmail": "3.7.0.150", - "AspNetCoreRateLimit": "4.0.2", - "AspNetCoreRateLimit.Redis": "1.0.1", - "Azure.Extensions.AspNetCore.DataProtection.Blobs": "1.2.1", - "Azure.Storage.Blobs": "12.11.0", - "Azure.Storage.Queues": "12.9.0", - "BitPay.Light": "1.0.1907", - "Braintree": "5.12.0", - "Fido2.AspNet": "3.0.0-beta2", - "Handlebars.Net": "2.1.2", - "IdentityServer4": "4.1.2", - "IdentityServer4.AccessTokenValidation": "3.0.1", - "MailKit": "3.2.0", - "Microsoft.AspNetCore.Authentication.JwtBearer": "6.0.4", - "Microsoft.Azure.Cosmos.Table": "1.0.8", - "Microsoft.Azure.NotificationHubs": "4.1.0", - "Microsoft.Azure.ServiceBus": "5.2.0", - "Microsoft.Data.SqlClient": "4.1.0", - "Microsoft.Extensions.Caching.StackExchangeRedis": "6.0.6", - "Microsoft.Extensions.Configuration.EnvironmentVariables": "6.0.1", - "Microsoft.Extensions.Configuration.UserSecrets": "6.0.1", - "Microsoft.Extensions.Identity.Stores": "6.0.4", - "Newtonsoft.Json": "13.0.1", - "Otp.NET": "1.2.2", - "Quartz": "3.4.0", - "SendGrid": "9.27.0", - "Sentry.Serilog": "3.16.0", - "Serilog.AspNetCore": "5.0.0", - "Serilog.Extensions.Logging": "3.1.0", - "Serilog.Extensions.Logging.File": "2.0.0", - "Serilog.Sinks.AzureCosmosDB": "2.0.0", - "Serilog.Sinks.SyslogMessages": "2.0.6", - "Stripe.net": "40.0.0", - "YubicoDotNetClient": "1.2.0" + "AWSSDK.SQS": "[3.7.2.47, )", + "AWSSDK.SimpleEmail": "[3.7.0.150, )", + "AspNetCoreRateLimit": "[4.0.2, )", + "AspNetCoreRateLimit.Redis": "[1.0.1, )", + "Azure.Extensions.AspNetCore.DataProtection.Blobs": "[1.2.1, )", + "Azure.Storage.Blobs": "[12.11.0, )", + "Azure.Storage.Queues": "[12.9.0, )", + "BitPay.Light": "[1.0.1907, )", + "Braintree": "[5.12.0, )", + "Fido2.AspNet": "[3.0.0-beta2, )", + "Handlebars.Net": "[2.1.2, )", + "IdentityServer4": "[4.1.2, )", + "IdentityServer4.AccessTokenValidation": "[3.0.1, )", + "MailKit": "[3.2.0, )", + "Microsoft.AspNetCore.Authentication.JwtBearer": "[6.0.4, )", + "Microsoft.Azure.Cosmos.Table": "[1.0.8, )", + "Microsoft.Azure.NotificationHubs": "[4.1.0, )", + "Microsoft.Azure.ServiceBus": "[5.2.0, )", + "Microsoft.Data.SqlClient": "[4.1.0, )", + "Microsoft.Extensions.Caching.StackExchangeRedis": "[6.0.6, )", + "Microsoft.Extensions.Configuration.EnvironmentVariables": "[6.0.1, )", + "Microsoft.Extensions.Configuration.UserSecrets": "[6.0.1, )", + "Microsoft.Extensions.Identity.Stores": "[6.0.4, )", + "Newtonsoft.Json": "[13.0.1, )", + "Otp.NET": "[1.2.2, )", + "Quartz": "[3.4.0, )", + "SendGrid": "[9.27.0, )", + "Sentry.Serilog": "[3.16.0, )", + "Serilog.AspNetCore": "[5.0.0, )", + "Serilog.Extensions.Logging": "[3.1.0, )", + "Serilog.Extensions.Logging.File": "[2.0.0, )", + "Serilog.Sinks.AzureCosmosDB": "[2.0.0, )", + "Serilog.Sinks.SyslogMessages": "[2.0.6, )", + "Stripe.net": "[40.0.0, )", + "YubicoDotNetClient": "[1.2.0, )" + } + }, + "infrastructure.entityframework": { + "type": "Project", + "dependencies": { + "AutoMapper.Extensions.Microsoft.DependencyInjection": "[11.0.0, )", + "Core": "[2022.10.0, )", + "Microsoft.EntityFrameworkCore.Relational": "[6.0.4, )", + "Npgsql.EntityFrameworkCore.PostgreSQL": "[6.0.4, )", + "Pomelo.EntityFrameworkCore.MySql": "[6.0.1, )", + "linq2db.EntityFrameworkCore": "[6.7.1, )" } } } diff --git a/util/MySqlMigrations/Factories.cs b/util/MySqlMigrations/Factories.cs index 538c39612..c4d1d3176 100644 --- a/util/MySqlMigrations/Factories.cs +++ b/util/MySqlMigrations/Factories.cs @@ -4,14 +4,15 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; using Microsoft.Extensions.Configuration; -namespace MySqlMigrations; +namespace Bit.MySqlMigrations; public static class GlobalSettingsFactory { public static GlobalSettings GlobalSettings { get; } = new GlobalSettings(); static GlobalSettingsFactory() { - var configBuilder = new ConfigurationBuilder().AddUserSecrets(); + // UserSecretsId here should match what is in Api.csproj + var configBuilder = new ConfigurationBuilder().AddUserSecrets("bitwarden-Api"); var Configuration = configBuilder.Build(); ConfigurationBinder.Bind(Configuration.GetSection("GlobalSettings"), GlobalSettings); } diff --git a/util/MySqlMigrations/Migrations/20220322191314_SelfHostF4E.cs b/util/MySqlMigrations/Migrations/20220322191314_SelfHostF4E.cs index 993399e50..2b3b3b396 100644 --- a/util/MySqlMigrations/Migrations/20220322191314_SelfHostF4E.cs +++ b/util/MySqlMigrations/Migrations/20220322191314_SelfHostF4E.cs @@ -1,4 +1,5 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Bit.EfShared; +using Microsoft.EntityFrameworkCore.Migrations; namespace Bit.MySqlMigrations.Migrations; diff --git a/util/MySqlMigrations/MySqlDbMigrator.cs b/util/MySqlMigrations/MySqlDbMigrator.cs new file mode 100644 index 000000000..837a5bc0d --- /dev/null +++ b/util/MySqlMigrations/MySqlDbMigrator.cs @@ -0,0 +1,41 @@ +using Bit.Core; +using Bit.Core.Utilities; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Bit.MySqlMigrations; + +public class MySqlDbMigrator : IDbMigrator +{ + private readonly IServiceScopeFactory _serviceScopeFactory; + private readonly ILogger _logger; + + public MySqlDbMigrator(IServiceScopeFactory serviceScopeFactory, ILogger logger) + { + _serviceScopeFactory = serviceScopeFactory; + _logger = logger; + } + + public bool MigrateDatabase(bool enableLogging = true, + CancellationToken cancellationToken = default(CancellationToken)) + { + if (enableLogging && _logger != null) + { + _logger.LogInformation(Constants.BypassFiltersEventId, "Migrating database."); + } + + using var scope = _serviceScopeFactory.CreateScope(); + var databaseContext = scope.ServiceProvider.GetRequiredService(); + databaseContext.Database.Migrate(); + + if (enableLogging && _logger != null) + { + _logger.LogInformation(Constants.BypassFiltersEventId, "Migration successful."); + } + + cancellationToken.ThrowIfCancellationRequested(); + return true; + } +} diff --git a/util/MySqlMigrations/MySqlMigrations.csproj b/util/MySqlMigrations/MySqlMigrations.csproj index 40e7fd88e..b1685132b 100644 --- a/util/MySqlMigrations/MySqlMigrations.csproj +++ b/util/MySqlMigrations/MySqlMigrations.csproj @@ -1,4 +1,4 @@ - + 9f1cd3e0-70f2-4921-8068-b2538fd7c3f7 @@ -6,7 +6,7 @@ - + diff --git a/util/PostgresMigrations/Factories.cs b/util/PostgresMigrations/Factories.cs index 189071590..8d17045a2 100644 --- a/util/PostgresMigrations/Factories.cs +++ b/util/PostgresMigrations/Factories.cs @@ -4,14 +4,15 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; using Microsoft.Extensions.Configuration; -namespace MySqlMigrations; +namespace Bit.PostgresMigrations; public static class GlobalSettingsFactory { public static GlobalSettings GlobalSettings { get; } = new GlobalSettings(); static GlobalSettingsFactory() { - var configBuilder = new ConfigurationBuilder().AddUserSecrets(); + // UserSecretsId here should match what is in Api.csproj + var configBuilder = new ConfigurationBuilder().AddUserSecrets("bitwarden-Api"); var Configuration = configBuilder.Build(); ConfigurationBinder.Bind(Configuration.GetSection("GlobalSettings"), GlobalSettings); } diff --git a/util/PostgresMigrations/Migrations/20220322183505_SelfHostF4E.cs b/util/PostgresMigrations/Migrations/20220322183505_SelfHostF4E.cs index 0c030f0dd..de8eccbd1 100644 --- a/util/PostgresMigrations/Migrations/20220322183505_SelfHostF4E.cs +++ b/util/PostgresMigrations/Migrations/20220322183505_SelfHostF4E.cs @@ -1,4 +1,5 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Bit.EfShared; +using Microsoft.EntityFrameworkCore.Migrations; namespace Bit.PostgresMigrations.Migrations; diff --git a/util/PostgresMigrations/PostgresDbMigratorDbMigrator.cs b/util/PostgresMigrations/PostgresDbMigratorDbMigrator.cs new file mode 100644 index 000000000..18a71031f --- /dev/null +++ b/util/PostgresMigrations/PostgresDbMigratorDbMigrator.cs @@ -0,0 +1,41 @@ +using Bit.Core; +using Bit.Core.Utilities; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Bit.PostgresMigrations; + +public class PostgresDbMigrator : IDbMigrator +{ + private readonly IServiceScopeFactory _serviceScopeFactory; + private readonly ILogger _logger; + + public PostgresDbMigrator(IServiceScopeFactory serviceScopeFactory, ILogger logger) + { + _serviceScopeFactory = serviceScopeFactory; + _logger = logger; + } + + public bool MigrateDatabase(bool enableLogging = true, + CancellationToken cancellationToken = default(CancellationToken)) + { + if (enableLogging && _logger != null) + { + _logger.LogInformation(Constants.BypassFiltersEventId, "Migrating database."); + } + + using var scope = _serviceScopeFactory.CreateScope(); + var databaseContext = scope.ServiceProvider.GetRequiredService(); + databaseContext.Database.Migrate(); + + if (enableLogging && _logger != null) + { + _logger.LogInformation(Constants.BypassFiltersEventId, "Migration successful."); + } + + cancellationToken.ThrowIfCancellationRequested(); + return true; + } +} diff --git a/util/PostgresMigrations/PostgresMigrations.csproj b/util/PostgresMigrations/PostgresMigrations.csproj index a4259901a..02d851395 100644 --- a/util/PostgresMigrations/PostgresMigrations.csproj +++ b/util/PostgresMigrations/PostgresMigrations.csproj @@ -2,7 +2,7 @@ - +