1
0
mirror of https://github.com/bitwarden/browser.git synced 2025-02-12 00:41:29 +01:00

Merge branch 'main' into pm-13345-Add-Remove-Bitwarden-Families-policy-in-Admin-Console

This commit is contained in:
Cy Okeke 2024-11-08 10:26:51 +01:00
commit cf81e6af1a
No known key found for this signature in database
GPG Key ID: 88B341B55C84B45C
156 changed files with 2330 additions and 1128 deletions

50
.github/renovate.json vendored
View File

@ -41,16 +41,12 @@
}, },
{ {
"matchPackageNames": [ "matchPackageNames": [
"@ngtools/webpack",
"base64-loader", "base64-loader",
"buffer", "buffer",
"bufferutil", "bufferutil",
"copy-webpack-plugin",
"core-js", "core-js",
"css-loader", "css-loader",
"html-loader", "html-loader",
"html-webpack-injector",
"html-webpack-plugin",
"mini-css-extract-plugin", "mini-css-extract-plugin",
"ngx-infinite-scroll", "ngx-infinite-scroll",
"postcss", "postcss",
@ -60,20 +56,15 @@
"sass-loader", "sass-loader",
"style-loader", "style-loader",
"ts-loader", "ts-loader",
"tsconfig-paths-webpack-plugin",
"url", "url",
"util", "util"
"webpack",
"webpack-cli",
"webpack-dev-server",
"webpack-node-externals"
], ],
"description": "Admin Console owned dependencies", "description": "Admin Console owned dependencies",
"commitMessagePrefix": "[deps] AC:", "commitMessagePrefix": "[deps] AC:",
"reviewers": ["team:team-admin-console-dev"] "reviewers": ["team:team-admin-console-dev"]
}, },
{ {
"matchPackageNames": ["@types/node-ipc", "node-ipc", "qrious"], "matchPackageNames": ["qrious"],
"description": "Auth owned dependencies", "description": "Auth owned dependencies",
"commitMessagePrefix": "[deps] Auth:", "commitMessagePrefix": "[deps] Auth:",
"reviewers": ["team:team-auth-dev"] "reviewers": ["team:team-auth-dev"]
@ -110,27 +101,43 @@
}, },
{ {
"matchPackageNames": [ "matchPackageNames": [
"@babel/core",
"@babel/preset-env",
"@electron/notarize", "@electron/notarize",
"@electron/rebuild", "@electron/rebuild",
"@types/argon2-browser", "@ngtools/webpack",
"@types/chrome", "@types/chrome",
"@types/firefox-webext-browser", "@types/firefox-webext-browser",
"@types/glob",
"@types/jquery", "@types/jquery",
"@types/lowdb",
"@types/node", "@types/node",
"@types/node-forge", "@types/node-forge",
"argon2", "@types/node-ipc",
"argon2-browser", "@yao-pkg",
"big-integer", "babel-loader",
"browserslist",
"copy-webpack-plugin",
"electron",
"electron-builder", "electron-builder",
"electron-log", "electron-log",
"electron-reload", "electron-reload",
"electron-store", "electron-store",
"electron-updater", "electron-updater",
"electron", "html-webpack-injector",
"html-webpack-plugin",
"lowdb",
"node-forge", "node-forge",
"node-ipc",
"pkg",
"rxjs", "rxjs",
"tsconfig-paths-webpack-plugin",
"type-fest", "type-fest",
"typescript" "typescript",
"webpack",
"webpack-cli",
"webpack-dev-server",
"webpack-node-externals"
], ],
"description": "Platform owned dependencies", "description": "Platform owned dependencies",
"commitMessagePrefix": "[deps] Platform:", "commitMessagePrefix": "[deps] Platform:",
@ -231,7 +238,6 @@
"@types/koa__router", "@types/koa__router",
"@types/koa-bodyparser", "@types/koa-bodyparser",
"@types/koa-json", "@types/koa-json",
"@types/lowdb",
"@types/lunr", "@types/lunr",
"@types/node-fetch", "@types/node-fetch",
"@types/proper-lockfile", "@types/proper-lockfile",
@ -244,18 +250,22 @@
"koa", "koa",
"koa-bodyparser", "koa-bodyparser",
"koa-json", "koa-json",
"lowdb",
"lunr", "lunr",
"multer", "multer",
"node-fetch", "node-fetch",
"open", "open",
"pkg",
"proper-lockfile", "proper-lockfile",
"qrcode-parser" "qrcode-parser"
], ],
"description": "Vault owned dependencies", "description": "Vault owned dependencies",
"commitMessagePrefix": "[deps] Vault:", "commitMessagePrefix": "[deps] Vault:",
"reviewers": ["team:team-vault-dev"] "reviewers": ["team:team-vault-dev"]
},
{
"matchPackageNames": ["@types/argon2-browser", "argon2", "argon2-browser", "big-integer"],
"description": "Key Management owned dependencies",
"commitMessagePrefix": "[deps] KM:",
"reviewers": ["team:team-key-management-dev"]
} }
], ],
"ignoreDeps": ["@types/koa-bodyparser", "bootstrap", "node-ipc", "node", "npm"] "ignoreDeps": ["@types/koa-bodyparser", "bootstrap", "node-ipc", "node", "npm"]

View File

@ -1,7 +1,8 @@
name: Build Browser name: Build Browser
on: on:
pull_request: pull_request_target:
types: [opened, synchronize]
branches-ignore: branches-ignore:
- 'l10n_master' - 'l10n_master'
- 'cf-pages' - 'cf-pages'
@ -33,6 +34,10 @@ defaults:
shell: bash shell: bash
jobs: jobs:
check-run:
name: Check PR run
uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
setup: setup:
name: Setup name: Setup
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
@ -41,8 +46,10 @@ jobs:
adj_build_number: ${{ steps.gen_vars.outputs.adj_build_number }} adj_build_number: ${{ steps.gen_vars.outputs.adj_build_number }}
node_version: ${{ steps.retrieve-node-version.outputs.node_version }} node_version: ${{ steps.retrieve-node-version.outputs.node_version }}
steps: steps:
- name: Checkout repo - name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Get Package Version - name: Get Package Version
id: gen_vars id: gen_vars
@ -71,8 +78,10 @@ jobs:
run: run:
working-directory: apps/browser working-directory: apps/browser
steps: steps:
- name: Checkout repo - name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Testing locales - extName length - name: Testing locales - extName length
run: | run: |
@ -109,8 +118,10 @@ jobs:
_BUILD_NUMBER: ${{ needs.setup.outputs.adj_build_number }} _BUILD_NUMBER: ${{ needs.setup.outputs.adj_build_number }}
_NODE_VERSION: ${{ needs.setup.outputs.node_version }} _NODE_VERSION: ${{ needs.setup.outputs.node_version }}
steps: steps:
- name: Checkout repo - name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up Node - name: Set up Node
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
@ -225,12 +236,15 @@ jobs:
needs: needs:
- setup - setup
- locales-test - locales-test
- check-run
env: env:
_BUILD_NUMBER: ${{ needs.setup.outputs.adj_build_number }} _BUILD_NUMBER: ${{ needs.setup.outputs.adj_build_number }}
_NODE_VERSION: ${{ needs.setup.outputs.node_version }} _NODE_VERSION: ${{ needs.setup.outputs.node_version }}
steps: steps:
- name: Checkout repo - name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up Node - name: Set up Node
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
@ -342,8 +356,10 @@ jobs:
- build - build
- build-safari - build-safari
steps: steps:
- name: Checkout repo - name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Login to Azure - name: Login to Azure
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
@ -381,7 +397,10 @@ jobs:
- crowdin-push - crowdin-push
steps: steps:
- name: Check if any job failed - name: Check if any job failed
if: (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc') && contains(needs.*.result, 'failure') if: |
github.event_name != 'pull_request_target'
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
&& contains(needs.*.result, 'failure')
run: exit 1 run: exit 1
- name: Login to Azure - Prod Subscription - name: Login to Azure - Prod Subscription

View File

@ -1,7 +1,8 @@
name: Build CLI name: Build CLI
on: on:
pull_request: pull_request_target:
types: [opened, synchronize]
branches-ignore: branches-ignore:
- 'l10n_master' - 'l10n_master'
- 'cf-pages' - 'cf-pages'
@ -34,6 +35,10 @@ defaults:
working-directory: apps/cli working-directory: apps/cli
jobs: jobs:
check-run:
name: Check PR run
uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
setup: setup:
name: Setup name: Setup
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
@ -41,8 +46,10 @@ jobs:
package_version: ${{ steps.retrieve-package-version.outputs.package_version }} package_version: ${{ steps.retrieve-package-version.outputs.package_version }}
node_version: ${{ steps.retrieve-node-version.outputs.node_version }} node_version: ${{ steps.retrieve-node-version.outputs.node_version }}
steps: steps:
- name: Checkout repo - name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Get Package Version - name: Get Package Version
id: retrieve-package-version id: retrieve-package-version
@ -58,7 +65,6 @@ jobs:
NODE_VERSION=${NODE_NVMRC/v/''} NODE_VERSION=${NODE_NVMRC/v/''}
echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT
cli: cli:
name: "${{ matrix.os.base }} - ${{ matrix.license_type.readable }}" name: "${{ matrix.os.base }} - ${{ matrix.license_type.readable }}"
strategy: strategy:
@ -82,8 +88,10 @@ jobs:
_WIN_PKG_FETCH_VERSION: 20.11.1 _WIN_PKG_FETCH_VERSION: 20.11.1
_WIN_PKG_VERSION: 3.5 _WIN_PKG_VERSION: 3.5
steps: steps:
- name: Checkout repo - name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Setup Unix Vars - name: Setup Unix Vars
run: | run: |
@ -160,8 +168,10 @@ jobs:
_WIN_PKG_FETCH_VERSION: 20.11.1 _WIN_PKG_FETCH_VERSION: 20.11.1
_WIN_PKG_VERSION: 3.5 _WIN_PKG_VERSION: 3.5
steps: steps:
- name: Checkout repo - name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Setup Windows builder - name: Setup Windows builder
run: | run: |
@ -310,8 +320,10 @@ jobs:
env: env:
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }} _PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
steps: steps:
- name: Checkout repo - name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Print environment - name: Print environment
run: | run: |
@ -386,10 +398,14 @@ jobs:
- cli - cli
- cli-windows - cli-windows
- snap - snap
- check-run
steps: steps:
- name: Check if any job failed - name: Check if any job failed
working-directory: ${{ github.workspace }} working-directory: ${{ github.workspace }}
if: (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc') && contains(needs.*.result, 'failure') if: |
github.event_name != 'pull_request_target'
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
&& contains(needs.*.result, 'failure')
run: exit 1 run: exit 1
- name: Login to Azure - Prod Subscription - name: Login to Azure - Prod Subscription

View File

@ -1,7 +1,8 @@
name: Build Desktop name: Build Desktop
on: on:
pull_request: pull_request_target:
types: [opened, synchronize]
branches-ignore: branches-ignore:
- 'l10n_master' - 'l10n_master'
- 'cf-pages' - 'cf-pages'
@ -32,12 +33,18 @@ defaults:
shell: bash shell: bash
jobs: jobs:
check-run:
name: Check PR run
uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
electron-verify: electron-verify:
name: Verify Electron Version name: Verify Electron Version
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- name: Checkout repo - name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Verify - name: Verify
run: | run: |
@ -65,8 +72,10 @@ jobs:
run: run:
working-directory: apps/desktop working-directory: apps/desktop
steps: steps:
- name: Checkout repo - name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Get Package Version - name: Get Package Version
id: retrieve-version id: retrieve-version
@ -138,8 +147,10 @@ jobs:
run: run:
working-directory: apps/desktop working-directory: apps/desktop
steps: steps:
- name: Checkout repo - name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up Node - name: Set up Node
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
@ -238,7 +249,9 @@ jobs:
windows: windows:
name: Windows Build name: Windows Build
runs-on: windows-2022 runs-on: windows-2022
needs: setup needs:
- setup
- check-run
defaults: defaults:
run: run:
shell: pwsh shell: pwsh
@ -248,8 +261,10 @@ jobs:
_NODE_VERSION: ${{ needs.setup.outputs.node_version }} _NODE_VERSION: ${{ needs.setup.outputs.node_version }}
NODE_OPTIONS: --max_old_space_size=4096 NODE_OPTIONS: --max_old_space_size=4096
steps: steps:
- name: Checkout repo - name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up Node - name: Set up Node
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
@ -447,7 +462,9 @@ jobs:
macos-build: macos-build:
name: MacOS Build name: MacOS Build
runs-on: macos-13 runs-on: macos-13
needs: setup needs:
- setup
- check-run
env: env:
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }} _PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
_NODE_VERSION: ${{ needs.setup.outputs.node_version }} _NODE_VERSION: ${{ needs.setup.outputs.node_version }}
@ -456,8 +473,10 @@ jobs:
run: run:
working-directory: apps/desktop working-directory: apps/desktop
steps: steps:
- name: Checkout repo - name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up Node - name: Set up Node
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
@ -622,8 +641,10 @@ jobs:
run: run:
working-directory: apps/desktop working-directory: apps/desktop
steps: steps:
- name: Checkout repo - name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up Node - name: Set up Node
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
@ -841,8 +862,10 @@ jobs:
run: run:
working-directory: apps/desktop working-directory: apps/desktop
steps: steps:
- name: Checkout repo - name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up Node - name: Set up Node
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
@ -1088,8 +1111,10 @@ jobs:
run: run:
working-directory: apps/desktop working-directory: apps/desktop
steps: steps:
- name: Checkout repo - name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up Node - name: Set up Node
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
@ -1279,8 +1304,10 @@ jobs:
- macos-package-mas - macos-package-mas
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- name: Checkout repo - name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Login to Azure - name: Login to Azure
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
@ -1323,7 +1350,10 @@ jobs:
- crowdin-push - crowdin-push
steps: steps:
- name: Check if any job failed - name: Check if any job failed
if: (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc') && contains(needs.*.result, 'failure') if: |
github.event_name != 'pull_request_target'
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
&& contains(needs.*.result, 'failure')
run: exit 1 run: exit 1
- name: Login to Azure - Prod Subscription - name: Login to Azure - Prod Subscription

View File

@ -1,7 +1,8 @@
name: Build Web name: Build Web
on: on:
pull_request: pull_request_target:
types: [opened, synchronize]
branches-ignore: branches-ignore:
- 'l10n_master' - 'l10n_master'
- 'cf-pages' - 'cf-pages'
@ -36,6 +37,10 @@ env:
_AZ_REGISTRY: bitwardenprod.azurecr.io _AZ_REGISTRY: bitwardenprod.azurecr.io
jobs: jobs:
check-run:
name: Check PR run
uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
setup: setup:
name: Setup name: Setup
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
@ -43,8 +48,10 @@ jobs:
version: ${{ steps.version.outputs.value }} version: ${{ steps.version.outputs.value }}
node_version: ${{ steps.retrieve-node-version.outputs.node_version }} node_version: ${{ steps.retrieve-node-version.outputs.node_version }}
steps: steps:
- name: Checkout repo - name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Get GitHub sha as version - name: Get GitHub sha as version
id: version id: version
@ -89,8 +96,10 @@ jobs:
git_metadata: true git_metadata: true
steps: steps:
- name: Checkout repo - name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up Node - name: Set up Node
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
@ -142,6 +151,7 @@ jobs:
needs: needs:
- setup - setup
- build-artifacts - build-artifacts
- check-run
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@ -155,8 +165,10 @@ jobs:
env: env:
_VERSION: ${{ needs.setup.outputs.version }} _VERSION: ${{ needs.setup.outputs.version }}
steps: steps:
- name: Checkout repo - name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Check Branch to Publish - name: Check Branch to Publish
env: env:
@ -250,11 +262,15 @@ jobs:
crowdin-push: crowdin-push:
name: Crowdin Push name: Crowdin Push
if: github.ref == 'refs/heads/main' if: github.ref == 'refs/heads/main'
needs: build-artifacts needs:
- build-artifacts
- check-run
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- name: Checkout repo - name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Login to Azure - name: Login to Azure
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
@ -282,9 +298,11 @@ jobs:
trigger-web-vault-deploy: trigger-web-vault-deploy:
name: Trigger web vault deploy name: Trigger web vault deploy
if: github.ref == 'refs/heads/main' if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main'
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
needs: build-artifacts needs:
- build-artifacts
- check-run
steps: steps:
- name: Login to Azure - CI Subscription - name: Login to Azure - CI Subscription
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
@ -326,7 +344,10 @@ jobs:
- trigger-web-vault-deploy - trigger-web-vault-deploy
steps: steps:
- name: Check if any job failed - name: Check if any job failed
if: (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc') && contains(needs.*.result, 'failure') if: |
github.event_name != 'pull_request_target'
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
&& contains(needs.*.result, 'failure')
run: exit 1 run: exit 1
- name: Login to Azure - Prod Subscription - name: Login to Azure - Prod Subscription

View File

@ -1,124 +1,130 @@
name: Version Bump name: Repository management
on: on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
task:
default: "Version Bump"
description: "Task to execute"
options:
- "Version Bump"
- "Version Bump and Cut rc"
required: true
type: choice
bump_browser: bump_browser:
description: "Bump Browser?" description: "Bump Browser version?"
type: boolean type: boolean
default: false default: false
bump_cli: bump_cli:
description: "Bump CLI?" description: "Bump CLI version?"
type: boolean type: boolean
default: false default: false
bump_desktop: bump_desktop:
description: "Bump Desktop?" description: "Bump Desktop version?"
type: boolean type: boolean
default: false default: false
bump_web: bump_web:
description: "Bump Web?" description: "Bump Web version?"
type: boolean type: boolean
default: false default: false
target_ref:
default: "main"
description: "Branch/Tag to target for cut"
required: true
type: string
version_number_override: version_number_override:
description: "New version override (leave blank for automatic calculation, example: '2024.1.0')" description: "New version override (leave blank for automatic calculation, example: '2024.1.0')"
required: false required: false
type: string type: string
cut_rc_branch:
description: "Cut RC branch?"
default: true
type: boolean
enable_slack_notification:
description: "Enable Slack notifications for upcoming release?"
default: false
type: boolean
jobs: jobs:
setup:
name: Setup
runs-on: ubuntu-24.04
outputs:
branch: ${{ steps.set-branch.outputs.branch }}
token: ${{ steps.app-token.outputs.token }}
steps:
- name: Set branch
id: set-branch
env:
TASK: ${{ inputs.task }}
run: |
if [[ "$TASK" == "Version Bump" ]]; then
BRANCH="none"
elif [[ "$TASK" == "Version Bump and Cut rc" ]]; then
BRANCH="rc"
fi
echo "branch=$BRANCH" >> $GITHUB_OUTPUT
- name: Generate GH App token
uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0
id: app-token
with:
app-id: ${{ secrets.BW_GHAPP_ID }}
private-key: ${{ secrets.BW_GHAPP_KEY }}
cut_branch:
name: Cut branch
if: ${{ needs.setup.outputs.branch == 'rc' }}
needs: setup
runs-on: ubuntu-24.04
steps:
- name: Check out target ref
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ inputs.target_ref }}
token: ${{ needs.setup.outputs.token }}
- name: Check if ${{ needs.setup.outputs.branch }} branch exists
env:
BRANCH_NAME: ${{ needs.setup.outputs.branch }}
run: |
if [[ $(git ls-remote --heads origin $BRANCH_NAME) ]]; then
echo "$BRANCH_NAME already exists! Please delete $BRANCH_NAME before running again." >> $GITHUB_STEP_SUMMARY
exit 1
fi
- name: Cut branch
env:
BRANCH_NAME: ${{ needs.setup.outputs.branch }}
run: |
git switch --quiet --create $BRANCH_NAME
git push --quiet --set-upstream origin $BRANCH_NAME
bump_version: bump_version:
name: Bump Version name: Bump Version
runs-on: ubuntu-22.04 if: ${{ always() }}
runs-on: ubuntu-24.04
needs:
- cut_branch
- setup
outputs: outputs:
version_browser: ${{ steps.set-final-version-output.outputs.version_browser }} version_browser: ${{ steps.set-final-version-output.outputs.version_browser }}
version_cli: ${{ steps.set-final-version-output.outputs.version_cli }} version_cli: ${{ steps.set-final-version-output.outputs.version_cli }}
version_desktop: ${{ steps.set-final-version-output.outputs.version_desktop }} version_desktop: ${{ steps.set-final-version-output.outputs.version_desktop }}
version_web: ${{ steps.set-final-version-output.outputs.version_web }} version_web: ${{ steps.set-final-version-output.outputs.version_web }}
steps: steps:
- name: Validate version input - name: Validate version input format
if: ${{ inputs.version_number_override != '' }} if: ${{ inputs.version_number_override != '' }}
uses: bitwarden/gh-actions/version-check@main uses: bitwarden/gh-actions/version-check@main
with: with:
version: ${{ inputs.version_number_override }} version: ${{ inputs.version_number_override }}
- name: Slack Notification Check - name: Check out branch
run: |
if [[ "${{ inputs.enable_slack_notification }}" == true ]]; then
echo "Slack notifications enabled."
else
echo "Slack notifications disabled."
fi
- name: Checkout Branch
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with: with:
ref: main ref: main
token: ${{ needs.setup.outputs.token }}
- name: Check if RC branch exists - name: Configure Git
if: ${{ inputs.cut_rc_branch == true }}
run: | run: |
remote_rc_branch_check=$(git ls-remote --heads origin rc | wc -l) git config --local user.email "actions@github.com"
if [[ "${remote_rc_branch_check}" -gt 0 ]]; then git config --local user.name "Github Actions"
echo "Remote RC branch exists."
echo "Please delete current RC branch before running again."
exit 1
fi
- name: Login to Azure - CI Subscription
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
- name: Retrieve secrets
id: retrieve-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: "bitwarden-ci"
secrets: "github-gpg-private-key,
github-gpg-private-key-passphrase"
- name: Import GPG key
uses: crazy-max/ghaction-import-gpg@cb9bde2e2525e640591a934b1fd28eef1dcaf5e5 # v6.2.0
with:
gpg_private_key: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key }}
passphrase: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key-passphrase }}
git_user_signingkey: true
git_commit_gpgsign: true
- name: Setup git
run: |
git config --local user.email "106330231+bitwarden-devops-bot@users.noreply.github.com"
git config --local user.name "bitwarden-devops-bot"
- name: Create Version Branch
id: create-branch
run: |
CLIENTS=()
if [[ ${{ inputs.bump_browser }} == true ]]; then
CLIENTS+=("browser")
fi
if [[ ${{ inputs.bump_cli }} == true ]]; then
CLIENTS+=("cli")
fi
if [[ ${{ inputs.bump_desktop }} == true ]]; then
CLIENTS+=("desktop")
fi
if [[ ${{ inputs.bump_web }} == true ]]; then
CLIENTS+=("web")
fi
printf -v joined '%s,' "${CLIENTS[@]}"
echo "client=${joined%,}" >> $GITHUB_OUTPUT
NAME=version_bump_${{ github.ref_name }}_$(date +"%Y-%m-%d")
git switch -c $NAME
echo "name=$NAME" >> $GITHUB_OUTPUT
######################## ########################
# VERSION BUMP SECTION # # VERSION BUMP SECTION #
@ -165,7 +171,9 @@ jobs:
- name: Bump Browser Version - Version Override - name: Bump Browser Version - Version Override
if: ${{ inputs.bump_browser == true && inputs.version_number_override != '' }} if: ${{ inputs.bump_browser == true && inputs.version_number_override != '' }}
id: bump-browser-version-override id: bump-browser-version-override
run: npm version --workspace=@bitwarden/browser ${{ inputs.version_number_override }} env:
VERSION: ${{ inputs.version_number_override }}
run: npm version --workspace=@bitwarden/browser $VERSION
- name: Bump Browser Version - Automatic Calculation - name: Bump Browser Version - Automatic Calculation
if: ${{ inputs.bump_browser == true && inputs.version_number_override == '' }} if: ${{ inputs.bump_browser == true && inputs.version_number_override == '' }}
@ -250,7 +258,9 @@ jobs:
- name: Bump CLI Version - Version Override - name: Bump CLI Version - Version Override
if: ${{ inputs.bump_cli == true && inputs.version_number_override != '' }} if: ${{ inputs.bump_cli == true && inputs.version_number_override != '' }}
id: bump-cli-version-override id: bump-cli-version-override
run: npm version --workspace=@bitwarden/cli ${{ inputs.version_number_override }} env:
VERSION: ${{ inputs.version_number_override }}
run: npm version --workspace=@bitwarden/cli $VERSION
- name: Bump CLI Version - Automatic Calculation - name: Bump CLI Version - Automatic Calculation
if: ${{ inputs.bump_cli == true && inputs.version_number_override == '' }} if: ${{ inputs.bump_cli == true && inputs.version_number_override == '' }}
@ -300,7 +310,9 @@ jobs:
- name: Bump Desktop Version - Root - Version Override - name: Bump Desktop Version - Root - Version Override
if: ${{ inputs.bump_desktop == true && inputs.version_number_override != '' }} if: ${{ inputs.bump_desktop == true && inputs.version_number_override != '' }}
id: bump-desktop-version-override id: bump-desktop-version-override
run: npm version --workspace=@bitwarden/desktop ${{ inputs.version_number_override }} env:
VERSION: ${{ inputs.version_number_override }}
run: npm version --workspace=@bitwarden/desktop $VERSION
- name: Bump Desktop Version - Root - Automatic Calculation - name: Bump Desktop Version - Root - Automatic Calculation
if: ${{ inputs.bump_desktop == true && inputs.version_number_override == '' }} if: ${{ inputs.bump_desktop == true && inputs.version_number_override == '' }}
@ -311,7 +323,9 @@ jobs:
- name: Bump Desktop Version - App - Version Override - name: Bump Desktop Version - App - Version Override
if: ${{ inputs.bump_desktop == true && inputs.version_number_override != '' }} if: ${{ inputs.bump_desktop == true && inputs.version_number_override != '' }}
run: npm version ${{ inputs.version_number_override }} env:
VERSION: ${{ inputs.version_number_override }}
run: npm version $VERSION
working-directory: "apps/desktop/src" working-directory: "apps/desktop/src"
- name: Bump Desktop Version - App - Automatic Calculation - name: Bump Desktop Version - App - Automatic Calculation
@ -362,7 +376,9 @@ jobs:
- name: Bump Web Version - Version Override - name: Bump Web Version - Version Override
if: ${{ inputs.bump_web == true && inputs.version_number_override != '' }} if: ${{ inputs.bump_web == true && inputs.version_number_override != '' }}
id: bump-web-version-override id: bump-web-version-override
run: npm version --workspace=@bitwarden/web-vault ${{ inputs.version_number_override }} env:
VERSION: ${{ inputs.version_number_override }}
run: npm version --workspace=@bitwarden/web-vault $VERSION
- name: Bump Web Version - Automatic Calculation - name: Bump Web Version - Automatic Calculation
if: ${{ inputs.bump_web == true && inputs.version_number_override == '' }} if: ${{ inputs.bump_web == true && inputs.version_number_override == '' }}
@ -375,27 +391,29 @@ jobs:
- name: Set final version output - name: Set final version output
id: set-final-version-output id: set-final-version-output
env:
VERSION: ${{ inputs.version_number_override }}
run: | run: |
if [[ "${{ steps.bump-browser-version-override.outcome }}" = "success" ]]; then if [[ "${{ steps.bump-browser-version-override.outcome }}" = "success" ]]; then
echo "version_browser=${{ inputs.version_number_override }}" >> $GITHUB_OUTPUT echo "version_browser=$VERSION" >> $GITHUB_OUTPUT
elif [[ "${{ steps.bump-browser-version-automatic.outcome }}" = "success" ]]; then elif [[ "${{ steps.bump-browser-version-automatic.outcome }}" = "success" ]]; then
echo "version_browser=${{ steps.calculate-next-browser-version.outputs.version }}" >> $GITHUB_OUTPUT echo "version_browser=${{ steps.calculate-next-browser-version.outputs.version }}" >> $GITHUB_OUTPUT
fi fi
if [[ "${{ steps.bump-cli-version-override.outcome }}" = "success" ]]; then if [[ "${{ steps.bump-cli-version-override.outcome }}" = "success" ]]; then
echo "version_cli=${{ inputs.version_number_override }}" >> $GITHUB_OUTPUT echo "version_cli=$VERSION" >> $GITHUB_OUTPUT
elif [[ "${{ steps.bump-cli-version-automatic.outcome }}" = "success" ]]; then elif [[ "${{ steps.bump-cli-version-automatic.outcome }}" = "success" ]]; then
echo "version_cli=${{ steps.calculate-next-cli-version.outputs.version }}" >> $GITHUB_OUTPUT echo "version_cli=${{ steps.calculate-next-cli-version.outputs.version }}" >> $GITHUB_OUTPUT
fi fi
if [[ "${{ steps.bump-desktop-version-override.outcome }}" = "success" ]]; then if [[ "${{ steps.bump-desktop-version-override.outcome }}" = "success" ]]; then
echo "version_desktop=${{ inputs.version_number_override }}" >> $GITHUB_OUTPUT echo "version_desktop=$VERSION" >> $GITHUB_OUTPUT
elif [[ "${{ steps.bump-desktop-version-automatic.outcome }}" = "success" ]]; then elif [[ "${{ steps.bump-desktop-version-automatic.outcome }}" = "success" ]]; then
echo "version_desktop=${{ steps.calculate-next-desktop-version.outputs.version }}" >> $GITHUB_OUTPUT echo "version_desktop=${{ steps.calculate-next-desktop-version.outputs.version }}" >> $GITHUB_OUTPUT
fi fi
if [[ "${{ steps.bump-web-version-override.outcome }}" = "success" ]]; then if [[ "${{ steps.bump-web-version-override.outcome }}" = "success" ]]; then
echo "version_web=${{ inputs.version_number_override }}" >> $GITHUB_OUTPUT echo "version_web=$VERSION" >> $GITHUB_OUTPUT
elif [[ "${{ steps.bump-web-version-automatic.outcome }}" = "success" ]]; then elif [[ "${{ steps.bump-web-version-automatic.outcome }}" = "success" ]]; then
echo "version_web=${{ steps.calculate-next-web-version.outputs.version }}" >> $GITHUB_OUTPUT echo "version_web=${{ steps.calculate-next-web-version.outputs.version }}" >> $GITHUB_OUTPUT
fi fi
@ -416,199 +434,52 @@ jobs:
- name: Push changes - name: Push changes
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }} if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
env: run: git push
PR_BRANCH: ${{ steps.create-branch.outputs.name }}
run: git push -u origin $PR_BRANCH
- name: Generate PR message
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
id: pr-message
run: |
MESSAGE=""
if [[ "${{ inputs.bump_browser }}" == "true" ]]; then
MESSAGE+=$' Browser version bump to ${{ steps.set-final-version-output.outputs.version_browser }}\n'
fi
if [[ "${{ inputs.bump_cli }}" == "true" ]]; then cherry_pick:
MESSAGE+=$' CLI version bump to ${{ steps.set-final-version-output.outputs.version_cli }}\n' name: Cherry-Pick Commit(s)
fi if: ${{ needs.setup.outputs.branch == 'rc' }}
runs-on: ubuntu-24.04
if [[ "${{ inputs.bump_desktop }}" == "true" ]]; then needs:
MESSAGE+=$' Desktop version bump to ${{ steps.set-final-version-output.outputs.version_desktop }}\n' - bump_version
fi - setup
if [[ "${{ inputs.bump_web }}" == "true" ]]; then
MESSAGE+=$' Web version bump to ${{ steps.set-final-version-output.outputs.version_web }}\n'
fi
echo "MESSAGE<<EOF" >> $GITHUB_ENV
echo "$MESSAGE" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
- name: Generate GH App token
uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0
id: app-token
with:
app-id: ${{ secrets.BW_GHAPP_ID }}
private-key: ${{ secrets.BW_GHAPP_KEY }}
owner: ${{ github.repository_owner }}
- name: Create Version PR
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
id: create-pr
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
PR_BRANCH: ${{ steps.create-branch.outputs.name }}
TITLE: "Bump client(s) version"
run: |
PR_URL=$(gh pr create --title "$TITLE" \
--base "main" \
--head "$PR_BRANCH" \
--label "version update" \
--label "automated pr" \
--body "
## Type of change
- [ ] Bug fix
- [ ] New feature development
- [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc)
- [ ] Build/deploy pipeline (DevOps)
- [X] Other
## Objective
$MESSAGE")
echo "pr_number=${PR_URL##*/}" >> $GITHUB_OUTPUT
- name: Approve PR
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
run: gh pr review $PR_NUMBER --approve
- name: Merge PR
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
run: gh pr merge $PR_NUMBER --squash --auto --delete-branch
- name: Report upcoming browser release version to Slack
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' && steps.set-final-version-output.outputs.version_browser != '' && inputs.enable_slack_notification == true }}
uses: bitwarden/gh-actions/report-upcoming-release-version@main
with:
version: ${{ steps.set-final-version-output.outputs.version_browser }}
project: browser
AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
- name: Report upcoming cli release version to Slack
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' && steps.set-final-version-output.outputs.version_cli != '' && inputs.enable_slack_notification == true }}
uses: bitwarden/gh-actions/report-upcoming-release-version@main
with:
version: ${{ steps.set-final-version-output.outputs.version_cli }}
project: cli
AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
- name: Report upcoming desktop release version to Slack
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' && steps.set-final-version-output.outputs.version_desktop != '' && inputs.enable_slack_notification == true }}
uses: bitwarden/gh-actions/report-upcoming-release-version@main
with:
version: ${{ steps.set-final-version-output.outputs.version_desktop }}
project: desktop
AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
- name: Report upcoming web release version to Slack
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' && steps.set-final-version-output.outputs.version_web != '' && inputs.enable_slack_notification == true }}
uses: bitwarden/gh-actions/report-upcoming-release-version@main
with:
version: ${{ steps.set-final-version-output.outputs.version_web }}
project: web
AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
cut_rc:
name: Cut RC branch
if: ${{ inputs.cut_rc_branch == true }}
needs: bump_version
runs-on: ubuntu-22.04
steps: steps:
- name: Checkout Branch - name: Check out main branch
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with: with:
ref: main ref: main
token: ${{ needs.setup.outputs.token }}
### Browser - name: Configure Git
- name: Browser - Verify version has been updated
if: ${{ inputs.bump_browser == true }}
env:
NEW_VERSION: ${{ needs.bump_version.outputs.version_browser }}
run: | run: |
# Wait for version to change. git config --local user.email "actions@github.com"
while : ; do git config --local user.name "Github Actions"
echo "Waiting for version to be updated..."
git pull --force
CURRENT_VERSION=$(cat package.json | jq -r '.version')
# If the versions don't match we continue the loop, otherwise we break out of the loop. - name: Perform cherry-pick(s)
[[ "$NEW_VERSION" != "$CURRENT_VERSION" ]] || break
sleep 10
done
working-directory: apps/browser
### CLI
- name: CLI - Verify version has been updated
if: ${{ inputs.bump_cli == true }}
env:
NEW_VERSION: ${{ needs.bump_version.outputs.version_cli }}
run: | run: |
# Wait for version to change. # Function for cherry-picking
while : ; do cherry_pick () {
echo "Waiting for version to be updated..." local package_path="apps/$1/package.json"
git pull --force local source_branch=$2
CURRENT_VERSION=$(cat package.json | jq -r '.version') local destination_branch=$3
# If the versions don't match we continue the loop, otherwise we break out of the loop. # Get project commit/version from source branch
[[ "$NEW_VERSION" != "$CURRENT_VERSION" ]] || break git switch $source_branch
sleep 10 SOURCE_COMMIT=$(git log --reverse --pretty=format:"%H" --max-count=1 $package_path)
done SOURCE_VERSION=$(cat $package_path | jq -r '.version')
working-directory: apps/cli
### Desktop # Get project commit/version from destination branch
- name: Desktop - Verify version has been updated git switch $destination_branch
if: ${{ inputs.bump_desktop == true }} DESTINATION_VERSION=$(cat $package_path | jq -r '.version')
env:
NEW_VERSION: ${{ needs.bump_version.outputs.version_desktop }}
run: |
# Wait for version to change.
while : ; do
echo "Waiting for version to be updated..."
git pull --force
CURRENT_VERSION=$(cat package.json | jq -r '.version')
# If the versions don't match we continue the loop, otherwise we break out of the loop. if [[ "$DESTINATION_VERSION" != "$SOURCE_VERSION" ]]; then
[[ "$NEW_VERSION" != "$CURRENT_VERSION" ]] || break git cherry-pick --strategy-option=theirs -x $SOURCE_COMMIT
sleep 10 git push -u origin $destination_branch
done fi
working-directory: apps/desktop
### Web # Cherry-pick from 'main' into 'rc'
- name: Web - Verify version has been updated cherry_pick browser main rc
if: ${{ inputs.bump_web == true }} cherry_pick cli main rc
env: cherry_pick desktop main rc
NEW_VERSION: ${{ needs.bump_version.outputs.version_web }} cherry_pick web main rc
run: |
# Wait for version to change.
while : ; do
echo "Waiting for version to be updated..."
git pull --force
CURRENT_VERSION=$(cat package.json | jq -r '.version')
# If the versions don't match we continue the loop, otherwise we break out of the loop.
[[ "$NEW_VERSION" != "$CURRENT_VERSION" ]] || break
sleep 10
done
working-directory: apps/web
- name: Cut RC branch
run: |
git switch --quiet --create rc
git push --quiet --set-upstream origin rc

View File

@ -8,27 +8,55 @@ on:
jobs: jobs:
bump-version: bump-version:
name: Bump Desktop Version name: Bump Desktop Version
runs-on: ubuntu-22.04 runs-on: ubuntu-24.04
steps: steps:
- name: Login to Azure - CI Subscription - name: Generate GH App token
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0
id: app-token
with: with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} app-id: ${{ secrets.BW_GHAPP_ID }}
private-key: ${{ secrets.BW_GHAPP_KEY }}
- name: Retrieve bot secrets - name: Check out target ref
id: retrieve-bot-secrets uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with: with:
keyvault: bitwarden-ci ref: main
secrets: "github-pat-bitwarden-devops-bot-repo-scope" token: ${{ steps.app-token.outputs.token }}
- name: Trigger Version Bump workflow - name: Configure Git
env:
GH_TOKEN: ${{ steps.retrieve-bot-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
run: | run: |
echo '{"cut_rc_branch": "false", git config --local user.email "actions@github.com"
"bump_browser": "false", git config --local user.name "Github Actions"
"bump_cli": "false",
"bump_desktop": "true", - name: Get current Desktop version
"bump_web": "false"}' | \ id: current-desktop-version
gh workflow run version-bump.yml --json --repo bitwarden/clients run: |
CURRENT_VERSION=$(cat package.json | jq -r '.version')
echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
working-directory: apps/desktop
- name: Calculate next Desktop release version
id: calculate-next-desktop-version
uses: bitwarden/gh-actions/version-next@main
with:
version: ${{ steps.current-desktop-version.outputs.version }}
- name: Bump Desktop Version - Root - Automatic Calculation
id: bump-desktop-version-automatic
env:
VERSION: ${{ steps.calculate-next-desktop-version.outputs.version }}
run: npm version --workspace=@bitwarden/desktop $VERSION
- name: Bump Desktop Version - App - Automatic Calculation
env:
VERSION: ${{ steps.calculate-next-desktop-version.outputs.version }}
run: npm version $VERSION
working-directory: "apps/desktop/src"
- name: Commit files
env:
VERSION: ${{ steps.calculate-next-desktop-version.outputs.version }}
run: git commit -m "Bumped Desktop client to $VERSION" -a
- name: Push changes
run: git push

View File

@ -2878,7 +2878,7 @@
"message": "E-Mail generieren" "message": "E-Mail generieren"
}, },
"generatorBoundariesHint": { "generatorBoundariesHint": {
"message": "Value must be between $MIN$ and $MAX$", "message": "Wert muss zwischen $MIN$ und $MAX$ liegen",
"description": "Explains spin box minimum and maximum values to the user", "description": "Explains spin box minimum and maximum values to the user",
"placeholders": { "placeholders": {
"min": { "min": {

View File

@ -20,16 +20,16 @@
"message": "Crear cuenta" "message": "Crear cuenta"
}, },
"newToBitwarden": { "newToBitwarden": {
"message": "New to Bitwarden?" "message": "¿Nuevo en Bitwarden?"
}, },
"logInWithPasskey": { "logInWithPasskey": {
"message": "Log in with passkey" "message": "Iniciar sesión con clave de acceso"
}, },
"useSingleSignOn": { "useSingleSignOn": {
"message": "Use single sign-on" "message": "Usar inicio de sesión único"
}, },
"welcomeBack": { "welcomeBack": {
"message": "Welcome back" "message": "Bienvenido de nuevo"
}, },
"setAStrongPassword": { "setAStrongPassword": {
"message": "Establece una contraseña fuerte" "message": "Establece una contraseña fuerte"
@ -84,7 +84,7 @@
"message": "Incorporarse a la organización" "message": "Incorporarse a la organización"
}, },
"joinOrganizationName": { "joinOrganizationName": {
"message": "Join $ORGANIZATIONNAME$", "message": "Unirse a $ORGANIZATIONNAME$",
"placeholders": { "placeholders": {
"organizationName": { "organizationName": {
"content": "$1", "content": "$1",
@ -120,7 +120,7 @@
"message": "Copiar contraseña" "message": "Copiar contraseña"
}, },
"copyPassphrase": { "copyPassphrase": {
"message": "Copy passphrase" "message": "Copiar frase de contraseña"
}, },
"copyNote": { "copyNote": {
"message": "Copiar nota" "message": "Copiar nota"
@ -153,7 +153,7 @@
"message": "Copiar número de licencia" "message": "Copiar número de licencia"
}, },
"copyCustomField": { "copyCustomField": {
"message": "Copy $FIELD$", "message": "Copiar $FIELD$",
"placeholders": { "placeholders": {
"field": { "field": {
"content": "$1", "content": "$1",
@ -162,13 +162,13 @@
} }
}, },
"copyWebsite": { "copyWebsite": {
"message": "Copy website" "message": "Copiar sitio web"
}, },
"copyNotes": { "copyNotes": {
"message": "Copy notes" "message": "Copiar notas"
}, },
"fill": { "fill": {
"message": "Fill", "message": "Rellenar",
"description": "This string is used on the vault page to indicate autofilling. Horizontal space is limited in the interface here so try and keep translations as concise as possible." "description": "This string is used on the vault page to indicate autofilling. Horizontal space is limited in the interface here so try and keep translations as concise as possible."
}, },
"autoFill": { "autoFill": {
@ -223,13 +223,13 @@
"message": "Añadir elemento" "message": "Añadir elemento"
}, },
"accountEmail": { "accountEmail": {
"message": "Account email" "message": "Correo electrónico de la cuenta"
}, },
"requestHint": { "requestHint": {
"message": "Request hint" "message": "Solicitar pista"
}, },
"requestPasswordHint": { "requestPasswordHint": {
"message": "Request password hint" "message": "Solicitar pista de la contraseña"
}, },
"enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou": { "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou": {
"message": "Enter your account email address and your password hint will be sent to you" "message": "Enter your account email address and your password hint will be sent to you"
@ -427,7 +427,7 @@
"message": "Generar contraseña" "message": "Generar contraseña"
}, },
"generatePassphrase": { "generatePassphrase": {
"message": "Generate passphrase" "message": "Generar frase de contraseña"
}, },
"regeneratePassword": { "regeneratePassword": {
"message": "Regenerar contraseña" "message": "Regenerar contraseña"
@ -567,7 +567,7 @@
"message": "Notas" "message": "Notas"
}, },
"privateNote": { "privateNote": {
"message": "Private note" "message": "Nota privada"
}, },
"note": { "note": {
"message": "Nota" "message": "Nota"
@ -624,7 +624,7 @@
"message": "Tiempo de sesión agotado" "message": "Tiempo de sesión agotado"
}, },
"vaultTimeoutHeader": { "vaultTimeoutHeader": {
"message": "Vault timeout" "message": "Tiempo de espera de la caja fuerte"
}, },
"otherOptions": { "otherOptions": {
"message": "Otras opciones" "message": "Otras opciones"
@ -645,13 +645,13 @@
"message": "Tu caja fuerte está bloqueada. Verifica tu identidad para continuar." "message": "Tu caja fuerte está bloqueada. Verifica tu identidad para continuar."
}, },
"yourVaultIsLockedV2": { "yourVaultIsLockedV2": {
"message": "Your vault is locked" "message": "Tu caja fuerte está bloqueada"
}, },
"yourAccountIsLocked": { "yourAccountIsLocked": {
"message": "Your account is locked" "message": "Tu cuenta está bloqueada"
}, },
"or": { "or": {
"message": "or" "message": "o"
}, },
"unlock": { "unlock": {
"message": "Desbloquear" "message": "Desbloquear"
@ -676,7 +676,7 @@
"message": "Tiempo de espera de la caja fuerte" "message": "Tiempo de espera de la caja fuerte"
}, },
"vaultTimeout1": { "vaultTimeout1": {
"message": "Timeout" "message": "Tiempo de espera"
}, },
"lockNow": { "lockNow": {
"message": "Bloquear" "message": "Bloquear"
@ -4708,11 +4708,11 @@
"description": "Represents the - key in screen reader content as a readable word" "description": "Represents the - key in screen reader content as a readable word"
}, },
"plusCharacterDescriptor": { "plusCharacterDescriptor": {
"message": "Plus", "message": "s",
"description": "Represents the + key in screen reader content as a readable word" "description": "Represents the + key in screen reader content as a readable word"
}, },
"equalsCharacterDescriptor": { "equalsCharacterDescriptor": {
"message": "Equals", "message": "Igual",
"description": "Represents the = key in screen reader content as a readable word" "description": "Represents the = key in screen reader content as a readable word"
}, },
"braceLeftCharacterDescriptor": { "braceLeftCharacterDescriptor": {
@ -4736,15 +4736,15 @@
"description": "Represents the | key in screen reader content as a readable word" "description": "Represents the | key in screen reader content as a readable word"
}, },
"backSlashCharacterDescriptor": { "backSlashCharacterDescriptor": {
"message": "Back slash", "message": "Contrabarra",
"description": "Represents the back slash key in screen reader content as a readable word" "description": "Represents the back slash key in screen reader content as a readable word"
}, },
"colonCharacterDescriptor": { "colonCharacterDescriptor": {
"message": "Colon", "message": "Dos puntos",
"description": "Represents the : key in screen reader content as a readable word" "description": "Represents the : key in screen reader content as a readable word"
}, },
"semicolonCharacterDescriptor": { "semicolonCharacterDescriptor": {
"message": "Semicolon", "message": "Punto y coma",
"description": "Represents the ; key in screen reader content as a readable word" "description": "Represents the ; key in screen reader content as a readable word"
}, },
"doubleQuoteCharacterDescriptor": { "doubleQuoteCharacterDescriptor": {

View File

@ -1424,7 +1424,7 @@
"message": "Palvelimen URL" "message": "Palvelimen URL"
}, },
"selfHostBaseUrl": { "selfHostBaseUrl": {
"message": "Self-host server URL", "message": "Itse ylläpidetyn palvelimen URL-osoite",
"description": "Label for field requesting a self-hosted integration service URL" "description": "Label for field requesting a self-hosted integration service URL"
}, },
"apiUrl": { "apiUrl": {
@ -1795,13 +1795,13 @@
"message": "Salasanahistoria" "message": "Salasanahistoria"
}, },
"generatorHistory": { "generatorHistory": {
"message": "Generator history" "message": "Generaattorihistoria"
}, },
"clearGeneratorHistoryTitle": { "clearGeneratorHistoryTitle": {
"message": "Clear generator history" "message": "Tyhjennä generaattorihistoria"
}, },
"cleargGeneratorHistoryDescription": { "cleargGeneratorHistoryDescription": {
"message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" "message": "Jos jatkat, kaikki generaattorihistorian kohteet poistetaan. Haluatko varmasti jatkaa?"
}, },
"back": { "back": {
"message": "Takaisin" "message": "Takaisin"
@ -1920,7 +1920,7 @@
"message": "Tyhjennä historia" "message": "Tyhjennä historia"
}, },
"nothingToShow": { "nothingToShow": {
"message": "Nothing to show" "message": "Mitään näytettävää ei ole"
}, },
"nothingGeneratedRecently": { "nothingGeneratedRecently": {
"message": "Et ole luonut mitään hiljattain" "message": "Et ole luonut mitään hiljattain"
@ -2710,7 +2710,7 @@
"description": "Used as a card title description on the set password page to explain why the user is there" "description": "Used as a card title description on the set password page to explain why the user is there"
}, },
"cardMetrics": { "cardMetrics": {
"message": "out of $TOTAL$", "message": "/$TOTAL$",
"placeholders": { "placeholders": {
"total": { "total": {
"content": "$1", "content": "$1",

View File

@ -147,7 +147,7 @@
"message": "Kopiera personnummer" "message": "Kopiera personnummer"
}, },
"copyPassportNumber": { "copyPassportNumber": {
"message": "Copy passport number" "message": "Kopiera passnummer"
}, },
"copyLicenseNumber": { "copyLicenseNumber": {
"message": "Copy license number" "message": "Copy license number"
@ -4624,7 +4624,7 @@
"message": "Items that have been in trash more than 30 days will automatically be deleted" "message": "Items that have been in trash more than 30 days will automatically be deleted"
}, },
"restore": { "restore": {
"message": "Restore" "message": "Återställ"
}, },
"deleteForever": { "deleteForever": {
"message": "Delete forever" "message": "Delete forever"
@ -4744,7 +4744,7 @@
"description": "Represents the : key in screen reader content as a readable word" "description": "Represents the : key in screen reader content as a readable word"
}, },
"semicolonCharacterDescriptor": { "semicolonCharacterDescriptor": {
"message": "Semicolon", "message": "Semikolon",
"description": "Represents the ; key in screen reader content as a readable word" "description": "Represents the ; key in screen reader content as a readable word"
}, },
"doubleQuoteCharacterDescriptor": { "doubleQuoteCharacterDescriptor": {
@ -4756,11 +4756,11 @@
"description": "Represents the ' key in screen reader content as a readable word" "description": "Represents the ' key in screen reader content as a readable word"
}, },
"lessThanCharacterDescriptor": { "lessThanCharacterDescriptor": {
"message": "Less than", "message": "Mindre än",
"description": "Represents the < key in screen reader content as a readable word" "description": "Represents the < key in screen reader content as a readable word"
}, },
"greaterThanCharacterDescriptor": { "greaterThanCharacterDescriptor": {
"message": "Greater than", "message": "Större än",
"description": "Represents the > key in screen reader content as a readable word" "description": "Represents the > key in screen reader content as a readable word"
}, },
"commaCharacterDescriptor": { "commaCharacterDescriptor": {
@ -4772,7 +4772,7 @@
"description": "Represents the . key in screen reader content as a readable word" "description": "Represents the . key in screen reader content as a readable word"
}, },
"questionCharacterDescriptor": { "questionCharacterDescriptor": {
"message": "Question mark", "message": "Frågetecken",
"description": "Represents the ? key in screen reader content as a readable word" "description": "Represents the ? key in screen reader content as a readable word"
}, },
"forwardSlashCharacterDescriptor": { "forwardSlashCharacterDescriptor": {

View File

@ -168,7 +168,7 @@
"message": "複製備註" "message": "複製備註"
}, },
"fill": { "fill": {
"message": "Fill", "message": "填入",
"description": "This string is used on the vault page to indicate autofilling. Horizontal space is limited in the interface here so try and keep translations as concise as possible." "description": "This string is used on the vault page to indicate autofilling. Horizontal space is limited in the interface here so try and keep translations as concise as possible."
}, },
"autoFill": { "autoFill": {
@ -458,7 +458,7 @@
"description": "deprecated. Use specialCharactersLabel instead." "description": "deprecated. Use specialCharactersLabel instead."
}, },
"include": { "include": {
"message": "Include", "message": "包含",
"description": "Card header for password generator include block" "description": "Card header for password generator include block"
}, },
"uppercaseDescription": { "uppercaseDescription": {
@ -730,10 +730,10 @@
"message": "安全" "message": "安全"
}, },
"confirmMasterPassword": { "confirmMasterPassword": {
"message": "Confirm master password" "message": "確認主密碼"
}, },
"masterPassword": { "masterPassword": {
"message": "Master password" "message": "主密碼"
}, },
"masterPassImportant": { "masterPassImportant": {
"message": "Your master password cannot be recovered if you forget it!" "message": "Your master password cannot be recovered if you forget it!"
@ -1092,10 +1092,10 @@
"message": "This file export will be password protected and require the file password to decrypt." "message": "This file export will be password protected and require the file password to decrypt."
}, },
"filePassword": { "filePassword": {
"message": "File password" "message": "檔案密碼"
}, },
"exportPasswordDescription": { "exportPasswordDescription": {
"message": "This password will be used to export and import this file" "message": "此密碼將用於匯出和匯入此檔案"
}, },
"accountRestrictedOptionDescription": { "accountRestrictedOptionDescription": {
"message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account."
@ -3542,7 +3542,7 @@
"description": "Screen reader text (aria-label) for new item button in overlay" "description": "Screen reader text (aria-label) for new item button in overlay"
}, },
"newLogin": { "newLogin": {
"message": "New login", "message": "新增登入資訊",
"description": "Button text to display within inline menu when there are no matching items on a login field" "description": "Button text to display within inline menu when there are no matching items on a login field"
}, },
"addNewLoginItemAria": { "addNewLoginItemAria": {

View File

@ -9,7 +9,7 @@
<ng-container slot="end"> <ng-container slot="end">
<app-pop-out></app-pop-out> <app-pop-out></app-pop-out>
<app-current-account *ngIf="showAcctSwitcher"></app-current-account> <app-current-account *ngIf="showAcctSwitcher && hasLoggedInAccount"></app-current-account>
</ng-container> </ng-container>
</popup-header> </popup-header>

View File

@ -15,6 +15,7 @@ import { PopOutComponent } from "../../../platform/popup/components/pop-out.comp
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component"; import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
import { CurrentAccountComponent } from "../account-switching/current-account.component"; import { CurrentAccountComponent } from "../account-switching/current-account.component";
import { AccountSwitcherService } from "../account-switching/services/account-switcher.service";
import { ExtensionBitwardenLogo } from "./extension-bitwarden-logo.icon"; import { ExtensionBitwardenLogo } from "./extension-bitwarden-logo.icon";
@ -50,6 +51,7 @@ export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy {
protected pageIcon: Icon; protected pageIcon: Icon;
protected showReadonlyHostname: boolean; protected showReadonlyHostname: boolean;
protected maxWidth: "md" | "3xl"; protected maxWidth: "md" | "3xl";
protected hasLoggedInAccount: boolean = false;
protected theme: string; protected theme: string;
protected logo = ExtensionBitwardenLogo; protected logo = ExtensionBitwardenLogo;
@ -59,6 +61,7 @@ export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy {
private route: ActivatedRoute, private route: ActivatedRoute,
private i18nService: I18nService, private i18nService: I18nService,
private extensionAnonLayoutWrapperDataService: AnonLayoutWrapperDataService, private extensionAnonLayoutWrapperDataService: AnonLayoutWrapperDataService,
private accountSwitcherService: AccountSwitcherService,
) {} ) {}
async ngOnInit(): Promise<void> { async ngOnInit(): Promise<void> {
@ -68,6 +71,12 @@ export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy {
// Listen for page changes and update the page data appropriately // Listen for page changes and update the page data appropriately
this.listenForPageDataChanges(); this.listenForPageDataChanges();
this.listenForServiceDataChanges(); this.listenForServiceDataChanges();
this.accountSwitcherService.availableAccounts$
.pipe(takeUntil(this.destroy$))
.subscribe((accounts) => {
this.hasLoggedInAccount = accounts.some((account) => account.id !== "addAccount");
});
} }
private listenForPageDataChanges() { private listenForPageDataChanges() {

View File

@ -27,6 +27,7 @@ import { ButtonModule, I18nMockService } from "@bitwarden/components";
import { RegistrationCheckEmailIcon } from "../../../../../../libs/auth/src/angular/icons/registration-check-email.icon"; import { RegistrationCheckEmailIcon } from "../../../../../../libs/auth/src/angular/icons/registration-check-email.icon";
import { PopupRouterCacheService } from "../../../platform/popup/view-cache/popup-router-cache.service"; import { PopupRouterCacheService } from "../../../platform/popup/view-cache/popup-router-cache.service";
import { AccountSwitcherService } from "../account-switching/services/account-switcher.service";
import { ExtensionAnonLayoutWrapperDataService } from "./extension-anon-layout-wrapper-data.service"; import { ExtensionAnonLayoutWrapperDataService } from "./extension-anon-layout-wrapper-data.service";
import { import {
@ -45,6 +46,7 @@ const decorators = (options: {
applicationVersion?: string; applicationVersion?: string;
clientType?: ClientType; clientType?: ClientType;
hostName?: string; hostName?: string;
accounts?: any[];
}) => { }) => {
return [ return [
componentWrapperDecorator( componentWrapperDecorator(
@ -83,6 +85,13 @@ const decorators = (options: {
}), }),
}, },
}, },
{
provide: AccountSwitcherService,
useValue: {
availableAccounts$: of(options.accounts || []),
SPECIAL_ADD_ACCOUNT_ID: "addAccount",
} as Partial<AccountSwitcherService>,
},
{ {
provide: AuthService, provide: AuthService,
useValue: { useValue: {
@ -300,3 +309,64 @@ export const DynamicContentExample: Story = {
], ],
}), }),
}; };
export const HasLoggedInAccountExample: Story = {
render: (args) => ({
props: args,
template: "<router-outlet></router-outlet>",
}),
decorators: decorators({
components: [DefaultPrimaryOutletExampleComponent],
routes: [
{
path: "**",
redirectTo: "has-logged-in-account",
pathMatch: "full",
},
{
path: "",
component: ExtensionAnonLayoutWrapperComponent,
children: [
{
path: "has-logged-in-account",
data: {
hasLoggedInAccount: true,
showAcctSwitcher: true,
},
children: [
{
path: "",
component: DefaultPrimaryOutletExampleComponent,
},
{
path: "",
component: DefaultSecondaryOutletExampleComponent,
outlet: "secondary",
},
{
path: "",
component: DefaultEnvSelectorOutletExampleComponent,
outlet: "environment-selector",
},
],
},
],
},
],
accounts: [
{
name: "Test User",
email: "testuser@bitwarden.com",
id: "123e4567-e89b-12d3-a456-426614174000",
server: "bitwarden.com",
status: 2,
isActive: false,
},
{
name: "addAccount",
id: "addAccount",
isActive: false,
},
],
}),
};

View File

@ -1,10 +1,12 @@
import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core"; import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms"; import { FormBuilder, Validators } from "@angular/forms";
import { Router } from "@angular/router"; import { ActivatedRoute, Router } from "@angular/router";
import { Subject, firstValueFrom, switchMap, takeUntil } from "rxjs"; import { Subject, firstValueFrom, switchMap, takeUntil, tap } from "rxjs";
import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/components/environment-selector.component"; import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/components/environment-selector.component";
import { LoginEmailServiceAbstraction, RegisterRouteService } from "@bitwarden/auth/common"; import { LoginEmailServiceAbstraction, RegisterRouteService } from "@bitwarden/auth/common";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { ToastService } from "@bitwarden/components"; import { ToastService } from "@bitwarden/components";
@ -38,9 +40,13 @@ export class HomeComponent implements OnInit, OnDestroy {
private accountSwitcherService: AccountSwitcherService, private accountSwitcherService: AccountSwitcherService,
private registerRouteService: RegisterRouteService, private registerRouteService: RegisterRouteService,
private toastService: ToastService, private toastService: ToastService,
private configService: ConfigService,
private route: ActivatedRoute,
) {} ) {}
async ngOnInit(): Promise<void> { async ngOnInit(): Promise<void> {
this.listenForUnauthUiRefreshFlagChanges();
const email = await firstValueFrom(this.loginEmailService.loginEmail$); const email = await firstValueFrom(this.loginEmailService.loginEmail$);
const rememberEmail = this.loginEmailService.getRememberEmail(); const rememberEmail = this.loginEmailService.getRememberEmail();
@ -70,6 +76,29 @@ export class HomeComponent implements OnInit, OnDestroy {
this.destroyed$.complete(); this.destroyed$.complete();
} }
private listenForUnauthUiRefreshFlagChanges() {
this.configService
.getFeatureFlag$(FeatureFlag.UnauthenticatedExtensionUIRefresh)
.pipe(
tap(async (flag) => {
// If the flag is turned ON, we must force a reload to ensure the correct UI is shown
if (flag) {
const uniqueQueryParams = {
...this.route.queryParams,
// adding a unique timestamp to the query params to force a reload
t: new Date().getTime().toString(),
};
await this.router.navigate(["/login"], {
queryParams: uniqueQueryParams,
});
}
}),
takeUntil(this.destroyed$),
)
.subscribe();
}
get availableAccounts$() { get availableAccounts$() {
return this.accountSwitcherService.availableAccounts$; return this.accountSwitcherService.availableAccounts$;
} }

View File

@ -219,8 +219,12 @@ export class AutofillComponent implements OnInit {
: AutofillOverlayVisibility.Off; : AutofillOverlayVisibility.Off;
await this.autofillSettingsService.setInlineMenuVisibility(newInlineMenuVisibilityValue); await this.autofillSettingsService.setInlineMenuVisibility(newInlineMenuVisibilityValue);
// No need to initiate browser permission request if a feature is being turned off
if (newInlineMenuVisibilityValue !== AutofillOverlayVisibility.Off) {
await this.requestPrivacyPermission(); await this.requestPrivacyPermission();
} }
}
async updateAutofillOnPageLoad() { async updateAutofillOnPageLoad() {
await this.autofillSettingsService.setAutofillOnPageLoad(this.enableAutofillOnPageLoad); await this.autofillSettingsService.setAutofillOnPageLoad(this.enableAutofillOnPageLoad);

View File

@ -58,11 +58,33 @@ export class BrowserApi {
} }
static async createWindow(options: chrome.windows.CreateData): Promise<chrome.windows.Window> { static async createWindow(options: chrome.windows.CreateData): Promise<chrome.windows.Window> {
return new Promise((resolve) => return new Promise((resolve) => {
chrome.windows.create(options, (window) => { chrome.windows.create(options, async (newWindow) => {
resolve(window); if (!BrowserApi.isSafariApi) {
}), return resolve(newWindow);
); }
// Safari doesn't close the default extension popup when a new window is created so we need to
// manually trigger the close by focusing the main window after the new window is created
const allWindows = await new Promise<chrome.windows.Window[]>((resolve) => {
chrome.windows.getAll({ windowTypes: ["normal"] }, (windows) => resolve(windows));
});
const mainWindow = allWindows.find((window) => window.id !== newWindow.id);
// No main window found, resolve the new window
if (mainWindow == null || !mainWindow.id) {
return resolve(newWindow);
}
// Focus the main window to close the extension popup
chrome.windows.update(mainWindow.id, { focused: true }, () => {
// Refocus the newly created window
chrome.windows.update(newWindow.id, { focused: true }, () => {
resolve(newWindow);
});
});
});
});
} }
/** /**

View File

@ -24,9 +24,9 @@ import {
SendFormConfig, SendFormConfig,
SendFormConfigService, SendFormConfigService,
SendFormMode, SendFormMode,
SendFormModule,
} from "@bitwarden/send-ui"; } from "@bitwarden/send-ui";
import { SendFormModule } from "../../../../../../../libs/tools/send/send-ui/src/send-form/send-form.module";
import { PopupFooterComponent } from "../../../../platform/popup/layout/popup-footer.component"; import { PopupFooterComponent } from "../../../../platform/popup/layout/popup-footer.component";
import { PopupHeaderComponent } from "../../../../platform/popup/layout/popup-header.component"; import { PopupHeaderComponent } from "../../../../platform/popup/layout/popup-header.component";
import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page.component"; import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page.component";

View File

@ -39,9 +39,9 @@ dependencies = [
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.86" version = "1.0.93"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775"
[[package]] [[package]]
name = "arboard" name = "arboard"
@ -388,9 +388,9 @@ dependencies = [
[[package]] [[package]]
name = "core-foundation" name = "core-foundation"
version = "0.9.4" version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63"
dependencies = [ dependencies = [
"core-foundation-sys", "core-foundation-sys",
"libc", "libc",
@ -1154,9 +1154,9 @@ dependencies = [
[[package]] [[package]]
name = "napi" name = "napi"
version = "2.16.11" version = "2.16.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53575dfa17f208dd1ce3a2da2da4659aae393b256a472f2738a8586a6c4107fd" checksum = "214f07a80874bb96a8433b3cdfc84980d56c7b02e1a0d7ba4ba0db5cef785e2b"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"ctor", "ctor",
@ -1667,9 +1667,9 @@ checksum = "a3cf7c11c38cb994f3d40e8a8cde3bbd1f72a435e4c49e85d6553d8312306152"
[[package]] [[package]]
name = "security-framework" name = "security-framework"
version = "2.11.0" version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" checksum = "f9d0283c0a4a22a0f1b0e4edca251aa20b92fc96eaa09b84bec052f9415e9d71"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"core-foundation", "core-foundation",
@ -1680,9 +1680,9 @@ dependencies = [
[[package]] [[package]]
name = "security-framework-sys" name = "security-framework-sys"
version = "2.11.0" version = "2.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7" checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6"
dependencies = [ dependencies = [
"core-foundation-sys", "core-foundation-sys",
"libc", "libc",
@ -1867,18 +1867,18 @@ dependencies = [
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.61" version = "1.0.68"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" checksum = "02dd99dc800bbb97186339685293e1cc5d9df1f8fae2d0aecd9ff1c77efea892"
dependencies = [ dependencies = [
"thiserror-impl", "thiserror-impl",
] ]
[[package]] [[package]]
name = "thiserror-impl" name = "thiserror-impl"
version = "1.0.61" version = "1.0.68"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" checksum = "a7c61ec9a6f64d2793d8a45faba21efbe3ced62a886d44c36a009b2b519b4c7e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -2487,9 +2487,9 @@ dependencies = [
[[package]] [[package]]
name = "zbus" name = "zbus"
version = "4.3.1" version = "4.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "851238c133804e0aa888edf4a0229481c753544ca12a60fd1c3230c8a500fe40" checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725"
dependencies = [ dependencies = [
"async-broadcast", "async-broadcast",
"async-executor", "async-executor",
@ -2525,9 +2525,9 @@ dependencies = [
[[package]] [[package]]
name = "zbus_macros" name = "zbus_macros"
version = "4.3.1" version = "4.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d5a3f12c20bd473be3194af6b49d50d7bb804ef3192dc70eddedb26b85d9da7" checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e"
dependencies = [ dependencies = [
"proc-macro-crate", "proc-macro-crate",
"proc-macro2", "proc-macro2",
@ -2583,9 +2583,9 @@ dependencies = [
[[package]] [[package]]
name = "zvariant" name = "zvariant"
version = "4.1.2" version = "4.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1724a2b330760dc7d2a8402d841119dc869ef120b139d29862d6980e9c75bfc9" checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe"
dependencies = [ dependencies = [
"endi", "endi",
"enumflags2", "enumflags2",
@ -2596,9 +2596,9 @@ dependencies = [
[[package]] [[package]]
name = "zvariant_derive" name = "zvariant_derive"
version = "4.1.2" version = "4.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55025a7a518ad14518fb243559c058a2e5b848b015e31f1d90414f36e3317859" checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449"
dependencies = [ dependencies = [
"proc-macro-crate", "proc-macro-crate",
"proc-macro2", "proc-macro2",
@ -2609,9 +2609,9 @@ dependencies = [
[[package]] [[package]]
name = "zvariant_utils" name = "zvariant_utils"
version = "2.0.0" version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc242db087efc22bd9ade7aa7809e4ba828132edc312871584a6b4391bdf8786" checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

View File

@ -23,7 +23,7 @@ sys = [
[dependencies] [dependencies]
aes = "=0.8.4" aes = "=0.8.4"
anyhow = "=1.0.86" anyhow = "=1.0.93"
arboard = { version = "=3.4.1", default-features = false, features = [ arboard = { version = "=3.4.1", default-features = false, features = [
"wayland-data-control", "wayland-data-control",
] } ] }
@ -38,7 +38,7 @@ rand = "=0.8.5"
retry = "=2.0.0" retry = "=2.0.0"
scopeguard = "=1.2.0" scopeguard = "=1.2.0"
sha2 = "=0.10.8" sha2 = "=0.10.8"
thiserror = "=1.0.61" thiserror = "=1.0.68"
tokio = { version = "=1.41.0", features = ["io-util", "sync", "macros"] } tokio = { version = "=1.41.0", features = ["io-util", "sync", "macros"] }
tokio-util = "=0.7.12" tokio-util = "=0.7.12"
typenum = "=1.17.0" typenum = "=1.17.0"
@ -61,12 +61,12 @@ windows = { version = "=0.57.0", features = [
keytar = "=0.1.6" keytar = "=0.1.6"
[target.'cfg(target_os = "macos")'.dependencies] [target.'cfg(target_os = "macos")'.dependencies]
core-foundation = { version = "=0.9.4", optional = true } core-foundation = { version = "=0.10.0", optional = true }
security-framework = { version = "=2.11.0", optional = true } security-framework = { version = "=3.0.0", optional = true }
security-framework-sys = { version = "=2.11.0", optional = true } security-framework-sys = { version = "=2.12.0", optional = true }
[target.'cfg(target_os = "linux")'.dependencies] [target.'cfg(target_os = "linux")'.dependencies]
gio = { version = "=0.19.5", optional = true } gio = { version = "=0.19.5", optional = true }
libsecret = { version = "=0.5.0", optional = true } libsecret = { version = "=0.5.0", optional = true }
zbus = { version = "=4.3.1", optional = true } zbus = { version = "=4.4.0", optional = true }
zbus_polkit = { version = "=4.0.0", optional = true } zbus_polkit = { version = "=4.0.0", optional = true }

View File

@ -14,9 +14,9 @@ default = []
manual_test = [] manual_test = []
[dependencies] [dependencies]
anyhow = "=1.0.86" anyhow = "=1.0.93"
desktop_core = { path = "../core" } desktop_core = { path = "../core" }
napi = { version = "=2.16.11", features = ["async"] } napi = { version = "=2.16.13", features = ["async"] }
napi-derive = "=2.16.12" napi-derive = "=2.16.12"
tokio = { version = "1.38.0" } tokio = { version = "1.38.0" }
tokio-util = "0.7.11" tokio-util = "0.7.11"

View File

@ -7,7 +7,7 @@ version = "0.0.0"
publish = false publish = false
[dependencies] [dependencies]
anyhow = "=1.0.86" anyhow = "=1.0.93"
desktop_core = { path = "../core", default-features = false } desktop_core = { path = "../core", default-features = false }
futures = "0.3.30" futures = "0.3.30"
log = "0.4.22" log = "0.4.22"

View File

@ -58,20 +58,24 @@ async function run(context) {
id = identities[0].id; id = identities[0].id;
} }
console.log(`Signing proxy binary before the main bundle, using identity '${id}'`); console.log(
`Signing proxy binary before the main bundle, using identity '${id}', for build ${context.electronPlatformName}`,
);
const appName = context.packager.appInfo.productFilename; const appName = context.packager.appInfo.productFilename;
const appPath = `${context.appOutDir}/${appName}.app`; const appPath = `${context.appOutDir}/${appName}.app`;
const proxyPath = path.join(appPath, "Contents", "MacOS", "desktop_proxy"); const proxyPath = path.join(appPath, "Contents", "MacOS", "desktop_proxy");
const inheritProxyPath = path.join(appPath, "Contents", "MacOS", "desktop_proxy.inherit");
const packageId = "com.bitwarden.desktop"; const packageId = "com.bitwarden.desktop";
if (is_mas) {
const entitlementsName = "entitlements.desktop_proxy.plist"; const entitlementsName = "entitlements.desktop_proxy.plist";
const entitlementsPath = path.join(__dirname, "..", "resources", entitlementsName); const entitlementsPath = path.join(__dirname, "..", "resources", entitlementsName);
child_process.execSync( child_process.execSync(
`codesign -s '${id}' -i ${packageId} -f --timestamp --options runtime --entitlements ${entitlementsPath} ${proxyPath}`, `codesign -s '${id}' -i ${packageId} -f --timestamp --options runtime --entitlements ${entitlementsPath} ${proxyPath}`,
); );
const inheritProxyPath = path.join(appPath, "Contents", "MacOS", "desktop_proxy.inherit");
const inheritEntitlementsName = "entitlements.desktop_proxy.inherit.plist"; const inheritEntitlementsName = "entitlements.desktop_proxy.inherit.plist";
const inheritEntitlementsPath = path.join( const inheritEntitlementsPath = path.join(
__dirname, __dirname,
@ -82,6 +86,18 @@ async function run(context) {
child_process.execSync( child_process.execSync(
`codesign -s '${id}' -i ${packageId} -f --timestamp --options runtime --entitlements ${inheritEntitlementsPath} ${inheritProxyPath}`, `codesign -s '${id}' -i ${packageId} -f --timestamp --options runtime --entitlements ${inheritEntitlementsPath} ${inheritProxyPath}`,
); );
} else {
// For non-Appstore builds, we don't need the inherit binary as they are not sandboxed,
// but we sign and include it anyway for consistency. It should be removed once DDG supports the proxy directly.
const entitlementsName = "entitlements.mac.plist";
const entitlementsPath = path.join(__dirname, "..", "resources", entitlementsName);
child_process.execSync(
`codesign -s '${id}' -i ${packageId} -f --timestamp --options runtime --entitlements ${entitlementsPath} ${proxyPath}`,
);
child_process.execSync(
`codesign -s '${id}' -i ${packageId} -f --timestamp --options runtime --entitlements ${entitlementsPath} ${inheritProxyPath}`,
);
}
} }
} }

View File

@ -1,7 +1,7 @@
import { Component, NgZone, OnDestroy, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; import { Component, NgZone, OnDestroy, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
import { FormBuilder } from "@angular/forms"; import { FormBuilder } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router"; import { ActivatedRoute, Router } from "@angular/router";
import { Subject, takeUntil } from "rxjs"; import { Subject, takeUntil, tap } from "rxjs";
import { LoginComponentV1 as BaseLoginComponent } from "@bitwarden/angular/auth/components/login-v1.component"; import { LoginComponentV1 as BaseLoginComponent } from "@bitwarden/angular/auth/components/login-v1.component";
import { FormValidationErrorsService } from "@bitwarden/angular/platform/abstractions/form-validation-errors.service"; import { FormValidationErrorsService } from "@bitwarden/angular/platform/abstractions/form-validation-errors.service";
@ -14,8 +14,10 @@ import {
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction"; import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@ -76,6 +78,7 @@ export class LoginComponentV1 extends BaseLoginComponent implements OnInit, OnDe
webAuthnLoginService: WebAuthnLoginServiceAbstraction, webAuthnLoginService: WebAuthnLoginServiceAbstraction,
registerRouteService: RegisterRouteService, registerRouteService: RegisterRouteService,
toastService: ToastService, toastService: ToastService,
private configService: ConfigService,
) { ) {
super( super(
devicesApiService, devicesApiService,
@ -105,6 +108,8 @@ export class LoginComponentV1 extends BaseLoginComponent implements OnInit, OnDe
} }
async ngOnInit() { async ngOnInit() {
this.listenForUnauthUiRefreshFlagChanges();
await super.ngOnInit(); await super.ngOnInit();
await this.getLoginWithDevice(this.loggedEmail); await this.getLoginWithDevice(this.loggedEmail);
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => { this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
@ -137,6 +142,29 @@ export class LoginComponentV1 extends BaseLoginComponent implements OnInit, OnDe
this.componentDestroyed$.complete(); this.componentDestroyed$.complete();
} }
private listenForUnauthUiRefreshFlagChanges() {
this.configService
.getFeatureFlag$(FeatureFlag.UnauthenticatedExtensionUIRefresh)
.pipe(
tap(async (flag) => {
// If the flag is turned ON, we must force a reload to ensure the correct UI is shown
if (flag) {
const uniqueQueryParams = {
...this.route.queryParams,
// adding a unique timestamp to the query params to force a reload
t: new Date().getTime().toString(),
};
await this.router.navigate(["/"], {
queryParams: uniqueQueryParams,
});
}
}),
takeUntil(this.destroy$),
)
.subscribe();
}
async settings() { async settings() {
const [modal, childComponent] = await this.modalService.openViewRef( const [modal, childComponent] = await this.modalService.openViewRef(
EnvironmentComponent, EnvironmentComponent,

View File

@ -1684,10 +1684,10 @@
"message": "Die Kontolöschung ist dauerhaft. Sie kann nicht rückgängig gemacht werden." "message": "Die Kontolöschung ist dauerhaft. Sie kann nicht rückgängig gemacht werden."
}, },
"cannotDeleteAccount": { "cannotDeleteAccount": {
"message": "Cannot delete account" "message": "Konto kann nicht gelöscht werden"
}, },
"cannotDeleteAccountDesc": { "cannotDeleteAccountDesc": {
"message": "This action cannot be completed because your account is owned by an organization. Contact your organization administrator for additional details." "message": "Diese Aktion kann nicht abgeschlossen werden, da dein Konto im Besitz einer Organisation ist. Kontaktiere deinen Organisationsadministrator für weitere Details."
}, },
"accountDeleted": { "accountDeleted": {
"message": "Konto gelöscht" "message": "Konto gelöscht"
@ -2394,7 +2394,7 @@
"message": "E-Mail generieren" "message": "E-Mail generieren"
}, },
"generatorBoundariesHint": { "generatorBoundariesHint": {
"message": "Value must be between $MIN$ and $MAX$", "message": "Wert muss zwischen $MIN$ und $MAX$ liegen",
"description": "Explains spin box minimum and maximum values to the user", "description": "Explains spin box minimum and maximum values to the user",
"placeholders": { "placeholders": {
"min": { "min": {

View File

@ -1800,7 +1800,7 @@
"description": "Used as a card title description on the set password page to explain why the user is there" "description": "Used as a card title description on the set password page to explain why the user is there"
}, },
"cardMetrics": { "cardMetrics": {
"message": "out of $TOTAL$", "message": "/$TOTAL$",
"placeholders": { "placeholders": {
"total": { "total": {
"content": "$1", "content": "$1",

View File

@ -40,9 +40,9 @@
></bit-nav-item> ></bit-nav-item>
</bit-nav-group> </bit-nav-group>
<bit-nav-item <bit-nav-item
*ngIf="isAccessIntelligenceFeatureEnabled" *ngIf="isRiskInsightsFeatureEnabled"
[text]="'accessIntelligence' | i18n" [text]="'riskInsights' | i18n"
route="access-intelligence" route="risk-insights"
></bit-nav-item> ></bit-nav-item>
<bit-nav-group <bit-nav-group
icon="bwi-billing" icon="bwi-billing"

View File

@ -51,7 +51,7 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy {
showPaymentAndHistory$: Observable<boolean>; showPaymentAndHistory$: Observable<boolean>;
hideNewOrgButton$: Observable<boolean>; hideNewOrgButton$: Observable<boolean>;
organizationIsUnmanaged$: Observable<boolean>; organizationIsUnmanaged$: Observable<boolean>;
isAccessIntelligenceFeatureEnabled = false; isRiskInsightsFeatureEnabled = false;
private _destroy = new Subject<void>(); private _destroy = new Subject<void>();
@ -71,7 +71,7 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy {
async ngOnInit() { async ngOnInit() {
document.body.classList.remove("layout_frontend"); document.body.classList.remove("layout_frontend");
this.isAccessIntelligenceFeatureEnabled = await this.configService.getFeatureFlag( this.isRiskInsightsFeatureEnabled = await this.configService.getFeatureFlag(
FeatureFlag.AccessIntelligence, FeatureFlag.AccessIntelligence,
); );

View File

@ -63,10 +63,10 @@ const routes: Routes = [
), ),
}, },
{ {
path: "access-intelligence", path: "risk-insights",
loadChildren: () => loadChildren: () =>
import("../../tools/access-intelligence/access-intelligence.module").then( import("../../tools/risk-insights/risk-insights.module").then(
(m) => m.AccessIntelligenceModule, (m) => m.RiskInsightsModule,
), ),
}, },
{ {

View File

@ -123,21 +123,23 @@ export class AccountComponent implements OnInit, OnDestroy {
this.canEditSubscription = organization.canEditSubscription; this.canEditSubscription = organization.canEditSubscription;
this.canUseApi = organization.useApi; this.canUseApi = organization.useApi;
// Update disabled states - reactive forms prefers not using disabled attribute
// Disabling these fields for self hosted orgs is deprecated // Disabling these fields for self hosted orgs is deprecated
// This block can be completely removed as part of // This block can be completely removed as part of
// https://bitwarden.atlassian.net/browse/PM-10863 // https://bitwarden.atlassian.net/browse/PM-10863
if (!this.limitCollectionCreationDeletionSplitFeatureFlagIsEnabled) { if (!this.limitCollectionCreationDeletionSplitFeatureFlagIsEnabled) {
if (!this.selfHosted) { if (!this.selfHosted) {
this.formGroup.get("orgName").enable();
this.collectionManagementFormGroup.get("limitCollectionCreationDeletion").enable(); this.collectionManagementFormGroup.get("limitCollectionCreationDeletion").enable();
this.collectionManagementFormGroup.get("allowAdminAccessToAllCollectionItems").enable(); this.collectionManagementFormGroup.get("allowAdminAccessToAllCollectionItems").enable();
} }
} }
if (!this.selfHosted && this.canEditSubscription) { // Update disabled states - reactive forms prefers not using disabled attribute
if (!this.selfHosted) {
this.formGroup.get("orgName").enable();
if (this.canEditSubscription) {
this.formGroup.get("billingEmail").enable(); this.formGroup.get("billingEmail").enable();
} }
}
// Org Response // Org Response
this.org = orgResponse; this.org = orgResponse;

View File

@ -48,16 +48,7 @@
}}</span> }}</span>
</dd> </dd>
<dt>{{ "nextCharge" | i18n }}</dt> <dt>{{ "nextCharge" | i18n }}</dt>
<dd *ngIf="!enableTimeThreshold"> <dd>
{{
nextInvoice
? (nextInvoice.date | date: "mediumDate") +
", " +
(nextInvoice.amount | currency: "$")
: "-"
}}
</dd>
<dd *ngIf="enableTimeThreshold">
{{ {{
nextInvoice nextInvoice
? (sub.subscription.periodEndDate | date: "mediumDate") + ? (sub.subscription.periodEndDate | date: "mediumDate") +

View File

@ -38,13 +38,9 @@ export class UserSubscriptionComponent implements OnInit {
sub: SubscriptionResponse; sub: SubscriptionResponse;
selfHosted = false; selfHosted = false;
cloudWebVaultUrl: string; cloudWebVaultUrl: string;
enableTimeThreshold: boolean;
cancelPromise: Promise<any>; cancelPromise: Promise<any>;
reinstatePromise: Promise<any>; reinstatePromise: Promise<any>;
protected enableTimeThreshold$ = this.configService.getFeatureFlag$(
FeatureFlag.EnableTimeThreshold,
);
protected deprecateStripeSourcesAPI$ = this.configService.getFeatureFlag$( protected deprecateStripeSourcesAPI$ = this.configService.getFeatureFlag$(
FeatureFlag.AC2476_DeprecateStripeSourcesAPI, FeatureFlag.AC2476_DeprecateStripeSourcesAPI,
@ -69,7 +65,6 @@ export class UserSubscriptionComponent implements OnInit {
async ngOnInit() { async ngOnInit() {
this.cloudWebVaultUrl = await firstValueFrom(this.environmentService.cloudWebVaultUrl$); this.cloudWebVaultUrl = await firstValueFrom(this.environmentService.cloudWebVaultUrl$);
await this.load(); await this.load();
this.enableTimeThreshold = await firstValueFrom(this.enableTimeThreshold$);
this.firstLoaded = true; this.firstLoaded = true;
} }

View File

@ -48,10 +48,7 @@
<dt [ngClass]="{ 'tw-text-danger': isExpired }"> <dt [ngClass]="{ 'tw-text-danger': isExpired }">
{{ "subscriptionExpiration" | i18n }} {{ "subscriptionExpiration" | i18n }}
</dt> </dt>
<dd [ngClass]="{ 'tw-text-danger': isExpired }" *ngIf="!enableTimeThreshold"> <dd [ngClass]="{ 'tw-text-danger': isExpired }">
{{ nextInvoice ? (nextInvoice.date | date: "mediumDate") : "-" }}
</dd>
<dd [ngClass]="{ 'tw-text-danger': isExpired }" *ngIf="enableTimeThreshold">
{{ nextInvoice ? (sub.subscription.periodEndDate | date: "mediumDate") : "-" }} {{ nextInvoice ? (sub.subscription.periodEndDate | date: "mediumDate") : "-" }}
</dd> </dd>
</ng-container> </ng-container>

View File

@ -52,7 +52,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
loading = true; loading = true;
locale: string; locale: string;
showUpdatedSubscriptionStatusSection$: Observable<boolean>; showUpdatedSubscriptionStatusSection$: Observable<boolean>;
enableTimeThreshold: boolean;
preSelectedProductTier: ProductTierType = ProductTierType.Free; preSelectedProductTier: ProductTierType = ProductTierType.Free;
showSubscription = true; showSubscription = true;
showSelfHost = false; showSelfHost = false;
@ -65,10 +64,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
FeatureFlag.EnableConsolidatedBilling, FeatureFlag.EnableConsolidatedBilling,
); );
protected enableTimeThreshold$ = this.configService.getFeatureFlag$(
FeatureFlag.EnableTimeThreshold,
);
protected enableUpgradePasswordManagerSub$ = this.configService.getFeatureFlag$( protected enableUpgradePasswordManagerSub$ = this.configService.getFeatureFlag$(
FeatureFlag.EnableUpgradePasswordManagerSub, FeatureFlag.EnableUpgradePasswordManagerSub,
); );
@ -117,7 +112,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
this.showUpdatedSubscriptionStatusSection$ = this.configService.getFeatureFlag$( this.showUpdatedSubscriptionStatusSection$ = this.configService.getFeatureFlag$(
FeatureFlag.AC1795_UpdatedSubscriptionStatusSection, FeatureFlag.AC1795_UpdatedSubscriptionStatusSection,
); );
this.enableTimeThreshold = await firstValueFrom(this.enableTimeThreshold$);
} }
ngOnDestroy() { ngOnDestroy() {
@ -298,9 +292,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
return this.i18nService.t("subscriptionUpgrade", this.sub.seats.toString()); return this.i18nService.t("subscriptionUpgrade", this.sub.seats.toString());
} }
} else if (this.sub.maxAutoscaleSeats === this.sub.seats && this.sub.seats != null) { } else if (this.sub.maxAutoscaleSeats === this.sub.seats && this.sub.seats != null) {
if (!this.enableTimeThreshold) {
return this.i18nService.t("subscriptionMaxReached", this.sub.seats.toString());
}
const seatAdjustmentMessage = this.sub.plan.isAnnual const seatAdjustmentMessage = this.sub.plan.isAnnual
? "annualSubscriptionUserSeatsMessage" ? "annualSubscriptionUserSeatsMessage"
: "monthlySubscriptionUserSeatsMessage"; : "monthlySubscriptionUserSeatsMessage";
@ -311,21 +302,11 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
} else if (this.userOrg.productTierType === ProductTierType.TeamsStarter) { } else if (this.userOrg.productTierType === ProductTierType.TeamsStarter) {
return this.i18nService.t("subscriptionUserSeatsWithoutAdditionalSeatsOption", 10); return this.i18nService.t("subscriptionUserSeatsWithoutAdditionalSeatsOption", 10);
} else if (this.sub.maxAutoscaleSeats == null) { } else if (this.sub.maxAutoscaleSeats == null) {
if (!this.enableTimeThreshold) {
return this.i18nService.t("subscriptionUserSeatsUnlimitedAutoscale");
}
const seatAdjustmentMessage = this.sub.plan.isAnnual const seatAdjustmentMessage = this.sub.plan.isAnnual
? "annualSubscriptionUserSeatsMessage" ? "annualSubscriptionUserSeatsMessage"
: "monthlySubscriptionUserSeatsMessage"; : "monthlySubscriptionUserSeatsMessage";
return this.i18nService.t(seatAdjustmentMessage); return this.i18nService.t(seatAdjustmentMessage);
} else { } else {
if (!this.enableTimeThreshold) {
return this.i18nService.t(
"subscriptionUserSeatsLimitedAutoscale",
this.sub.maxAutoscaleSeats.toString(),
);
}
const seatAdjustmentMessage = this.sub.plan.isAnnual const seatAdjustmentMessage = this.sub.plan.isAnnual
? "annualSubscriptionUserSeatsMessage" ? "annualSubscriptionUserSeatsMessage"
: "monthlySubscriptionUserSeatsMessage"; : "monthlySubscriptionUserSeatsMessage";

View File

@ -1,9 +0,0 @@
import { NgModule } from "@angular/core";
import { AccessIntelligenceRoutingModule } from "./access-intelligence-routing.module";
import { AccessIntelligenceComponent } from "./access-intelligence.component";
@NgModule({
imports: [AccessIntelligenceComponent, AccessIntelligenceRoutingModule],
})
export class AccessIntelligenceModule {}

View File

@ -1,120 +0,0 @@
<p>{{ "passwordsReportDesc" | i18n }}</p>
<div *ngIf="loading">
<i
class="bwi bwi-spinner bwi-spin tw-text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
</div>
<div class="tw-mt-4 tw-flex tw-flex-col" *ngIf="!loading && dataSource.data.length">
<div class="tw-flex tw-gap-6">
<tools-card
class="tw-flex-1"
[title]="'atRiskMembers' | i18n"
[value]="totalMembersMap.size - 3"
[maxValue]="totalMembersMap.size"
>
</tools-card>
<tools-card
class="tw-flex-1"
[title]="'atRiskApplications' | i18n"
[value]="totalMembersMap.size - 1"
[maxValue]="totalMembersMap.size"
>
</tools-card>
</div>
<div class="tw-flex tw-mt-8 tw-mb-4 tw-gap-4">
<bit-search class="tw-grow" [formControl]="searchControl"></bit-search>
<button class="tw-rounded-lg" type="button" buttonType="secondary" bitButton>
<i class="bwi bwi-star-f tw-mr-2"></i>
{{ "markAppAsCritical" | i18n }}
</button>
</div>
<div class="tw-mt-4 tw-flex tw-flex-col" *ngIf="!loading && dataSource.data.length">
<div class="tw-flex tw-gap-6">
<tools-card
class="tw-flex-1"
[title]="'atRiskMembers' | i18n"
[value]="totalMembersMap.size - 3"
[maxValue]="totalMembersMap.size"
>
</tools-card>
<tools-card
class="tw-flex-1"
[title]="'atRiskApplications' | i18n"
[value]="totalMembersMap.size - 1"
[maxValue]="totalMembersMap.size"
>
</tools-card>
</div>
<div class="tw-flex tw-mt-8 tw-mb-4 tw-gap-4">
<bit-search class="tw-grow" [formControl]="searchControl"></bit-search>
<button
class="tw-rounded-lg"
type="button"
buttonType="secondary"
[disabled]="!selectedIds.size"
bitButton
[bitAction]="markAppsAsCritical"
appA11yTitle="{{ 'markAppAsCritical' | i18n }}"
>
<i class="bwi bwi-star-f tw-mr-2"></i>
{{ "markAppAsCritical" | i18n }}
</button>
</div>
<bit-table [dataSource]="dataSource">
<ng-container header>
<tr bitRow>
<th bitCell></th>
<th bitCell bitSortable="name">{{ "name" | i18n }}</th>
<th bitCell class="tw-text-right">{{ "weakness" | i18n }}</th>
<th bitCell class="tw-text-right">{{ "timesReused" | i18n }}</th>
<th bitCell class="tw-text-right">{{ "timesExposed" | i18n }}</th>
<th bitCell class="tw-text-right">{{ "totalMembers" | i18n }}</th>
</tr>
</ng-container>
<ng-template body let-rows$>
<tr bitRow *ngFor="let r of rows$ | async; trackBy: trackByFunction">
<td bitCell>
<input
bitCheckbox
type="checkbox"
[checked]="selectedIds.has(r.id)"
(change)="onCheckboxChange(r.id, $event)"
/>
</td>
<td bitCell>
<ng-container>
<span>{{ r.name }}</span>
</ng-container>
<br />
<small>{{ r.subTitle }}</small>
</td>
<td bitCell class="tw-text-right">
<span
bitBadge
*ngIf="passwordStrengthMap.has(r.id)"
[variant]="passwordStrengthMap.get(r.id)[1]"
>
{{ passwordStrengthMap.get(r.id)[0] | i18n }}
</span>
</td>
<td bitCell class="tw-text-right">
<span bitBadge *ngIf="passwordUseMap.has(r.login.password)" variant="warning">
{{ "reusedXTimes" | i18n: passwordUseMap.get(r.login.password) }}
</span>
</td>
<td bitCell class="tw-text-right">
<span bitBadge *ngIf="exposedPasswordMap.has(r.id)" variant="warning">
{{ "exposedXTimes" | i18n: exposedPasswordMap.get(r.id) }}
</span>
</td>
<td bitCell class="tw-text-right" data-testid="total-membership">
{{ totalMembersMap.get(r.id) || 0 }}
</td>
</tr>
</ng-template>
</bit-table>
</div>
</div>

View File

@ -57,6 +57,7 @@
type="button" type="button"
buttonType="secondary" buttonType="secondary"
bitButton bitButton
*ngIf="isCritialAppsFeatureEnabled"
[disabled]="!selectedIds.size" [disabled]="!selectedIds.size"
[loading]="markingAsCritical" [loading]="markingAsCritical"
(click)="markAppsAsCritical()" (click)="markAppsAsCritical()"
@ -68,7 +69,7 @@
<bit-table [dataSource]="dataSource"> <bit-table [dataSource]="dataSource">
<ng-container header> <ng-container header>
<tr> <tr>
<th></th> <th *ngIf="isCritialAppsFeatureEnabled"></th>
<th bitSortable="name" bitCell>{{ "application" | i18n }}</th> <th bitSortable="name" bitCell>{{ "application" | i18n }}</th>
<th bitSortable="atRiskPasswords" bitCell>{{ "atRiskPasswords" | i18n }}</th> <th bitSortable="atRiskPasswords" bitCell>{{ "atRiskPasswords" | i18n }}</th>
<th bitSortable="totalPasswords" bitCell>{{ "totalPasswords" | i18n }}</th> <th bitSortable="totalPasswords" bitCell>{{ "totalPasswords" | i18n }}</th>
@ -78,7 +79,7 @@
</ng-container> </ng-container>
<ng-template body let-rows$> <ng-template body let-rows$>
<tr bitRow *ngFor="let r of rows$ | async; trackBy: trackByFunction"> <tr bitRow *ngFor="let r of rows$ | async; trackBy: trackByFunction">
<td> <td *ngIf="isCritialAppsFeatureEnabled">
<input <input
bitCheckbox bitCheckbox
type="checkbox" type="checkbox"

View File

@ -7,6 +7,8 @@ import { debounceTime, firstValueFrom, map } from "rxjs";
import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@ -41,6 +43,7 @@ export class AllApplicationsComponent implements OnInit {
protected organization: Organization; protected organization: Organization;
noItemsIcon = Icons.Security; noItemsIcon = Icons.Security;
protected markingAsCritical = false; protected markingAsCritical = false;
isCritialAppsFeatureEnabled = false;
// MOCK DATA // MOCK DATA
protected mockData = applicationTableMockData; protected mockData = applicationTableMockData;
@ -49,7 +52,7 @@ export class AllApplicationsComponent implements OnInit {
protected mockTotalMembersCount = 0; protected mockTotalMembersCount = 0;
protected mockTotalAppsCount = 0; protected mockTotalAppsCount = 0;
ngOnInit() { async ngOnInit() {
this.activatedRoute.paramMap this.activatedRoute.paramMap
.pipe( .pipe(
takeUntilDestroyed(this.destroyRef), takeUntilDestroyed(this.destroyRef),
@ -60,6 +63,10 @@ export class AllApplicationsComponent implements OnInit {
}), }),
) )
.subscribe(); .subscribe();
this.isCritialAppsFeatureEnabled = await this.configService.getFeatureFlag(
FeatureFlag.CriticalApps,
);
} }
constructor( constructor(
@ -70,6 +77,7 @@ export class AllApplicationsComponent implements OnInit {
protected activatedRoute: ActivatedRoute, protected activatedRoute: ActivatedRoute,
protected toastService: ToastService, protected toastService: ToastService,
protected organizationService: OrganizationService, protected organizationService: OrganizationService,
protected configService: ConfigService,
) { ) {
this.dataSource.data = applicationTableMockData; this.dataSource.data = applicationTableMockData;
this.searchControl.valueChanges this.searchControl.valueChanges

View File

@ -12,8 +12,8 @@ import { HeaderModule } from "../../layouts/header/header.module";
import { SharedModule } from "../../shared"; import { SharedModule } from "../../shared";
import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module"; import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module";
import { AccessIntelligenceTabType } from "./access-intelligence.component";
import { applicationTableMockData } from "./application-table.mock"; import { applicationTableMockData } from "./application-table.mock";
import { RiskInsightsTabType } from "./risk-insights.component";
@Component({ @Component({
standalone: true, standalone: true,
@ -49,8 +49,8 @@ export class CriticalApplicationsComponent implements OnInit {
} }
goToAllAppsTab = async () => { goToAllAppsTab = async () => {
await this.router.navigate([`organizations/${this.organizationId}/access-intelligence`], { await this.router.navigate([`organizations/${this.organizationId}/risk-insights`], {
queryParams: { tabIndex: AccessIntelligenceTabType.AllApps }, queryParams: { tabIndex: RiskInsightsTabType.AllApps },
queryParamsHandling: "merge", queryParamsHandling: "merge",
}); });
}; };

View File

@ -4,7 +4,7 @@ import { mock, MockProxy } from "jest-mock-extended";
import { of } from "rxjs"; import { of } from "rxjs";
// eslint-disable-next-line no-restricted-imports // eslint-disable-next-line no-restricted-imports
import { PasswordHealthService } from "@bitwarden/bit-common/tools/reports/access-intelligence"; import { PasswordHealthService } from "@bitwarden/bit-common/tools/reports/risk-insights";
import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";

View File

@ -6,7 +6,7 @@ import { map } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
// eslint-disable-next-line no-restricted-imports // eslint-disable-next-line no-restricted-imports
import { PasswordHealthService } from "@bitwarden/bit-common/tools/reports/access-intelligence"; import { PasswordHealthService } from "@bitwarden/bit-common/tools/reports/risk-insights";
import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";

View File

@ -0,0 +1,64 @@
<p>{{ "passwordsReportDesc" | i18n }}</p>
<div *ngIf="loading">
<i
class="bwi bwi-spinner bwi-spin tw-text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
</div>
<div class="tw-flex tw-flex-col" *ngIf="!loading && dataSource.data.length">
<bit-table [dataSource]="dataSource">
<ng-container header>
<tr bitRow>
<th bitCell></th>
<th bitCell bitSortable="name">{{ "name" | i18n }}</th>
<th bitCell class="tw-text-right">{{ "weakness" | i18n }}</th>
<th bitCell class="tw-text-right">{{ "timesReused" | i18n }}</th>
<th bitCell class="tw-text-right">{{ "timesExposed" | i18n }}</th>
<th bitCell class="tw-text-right">{{ "totalMembers" | i18n }}</th>
</tr>
</ng-container>
<ng-template body let-rows$>
<tr bitRow *ngFor="let r of rows$ | async; trackBy: trackByFunction">
<td bitCell>
<input
bitCheckbox
type="checkbox"
[checked]="selectedIds.has(r.id)"
(change)="onCheckboxChange(r.id, $event)"
/>
</td>
<td bitCell>
<ng-container>
<span>{{ r.name }}</span>
</ng-container>
<br />
<small>{{ r.subTitle }}</small>
</td>
<td bitCell class="tw-text-right">
<span
bitBadge
*ngIf="passwordStrengthMap.has(r.id)"
[variant]="passwordStrengthMap.get(r.id)[1]"
>
{{ passwordStrengthMap.get(r.id)[0] | i18n }}
</span>
</td>
<td bitCell class="tw-text-right">
<span bitBadge *ngIf="passwordUseMap.has(r.login.password)" variant="warning">
{{ "reusedXTimes" | i18n: passwordUseMap.get(r.login.password) }}
</span>
</td>
<td bitCell class="tw-text-right">
<span bitBadge *ngIf="exposedPasswordMap.has(r.id)" variant="warning">
{{ "exposedXTimes" | i18n: exposedPasswordMap.get(r.id) }}
</span>
</td>
<td bitCell class="tw-text-right" data-testid="total-membership">
{{ totalMembersMap.get(r.id) || 0 }}
</td>
</tr>
</ng-template>
</bit-table>
</div>

View File

@ -5,7 +5,7 @@ import { ActivatedRoute } from "@angular/router";
import { debounceTime, map } from "rxjs"; import { debounceTime, map } from "rxjs";
// eslint-disable-next-line no-restricted-imports // eslint-disable-next-line no-restricted-imports
import { PasswordHealthService } from "@bitwarden/bit-common/tools/reports/access-intelligence"; import { PasswordHealthService } from "@bitwarden/bit-common/tools/reports/risk-insights";
import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";

View File

@ -4,7 +4,7 @@ import { mock } from "jest-mock-extended";
import { of } from "rxjs"; import { of } from "rxjs";
// eslint-disable-next-line no-restricted-imports // eslint-disable-next-line no-restricted-imports
import { PasswordHealthService } from "@bitwarden/bit-common/tools/reports/access-intelligence"; import { PasswordHealthService } from "@bitwarden/bit-common/tools/reports/risk-insights";
import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";

View File

@ -6,7 +6,7 @@ import { map } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
// eslint-disable-next-line no-restricted-imports // eslint-disable-next-line no-restricted-imports
import { PasswordHealthService } from "@bitwarden/bit-common/tools/reports/access-intelligence"; import { PasswordHealthService } from "@bitwarden/bit-common/tools/reports/risk-insights";
import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";

View File

@ -4,15 +4,15 @@ import { RouterModule, Routes } from "@angular/router";
import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard"; import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { AccessIntelligenceComponent } from "./access-intelligence.component"; import { RiskInsightsComponent } from "./risk-insights.component";
const routes: Routes = [ const routes: Routes = [
{ {
path: "", path: "",
component: AccessIntelligenceComponent, component: RiskInsightsComponent,
canActivate: [canAccessFeature(FeatureFlag.AccessIntelligence)], canActivate: [canAccessFeature(FeatureFlag.AccessIntelligence)],
data: { data: {
titleId: "accessIntelligence", titleId: "RiskInsights",
}, },
}, },
]; ];
@ -21,4 +21,4 @@ const routes: Routes = [
imports: [RouterModule.forChild(routes)], imports: [RouterModule.forChild(routes)],
exports: [RouterModule], exports: [RouterModule],
}) })
export class AccessIntelligenceRoutingModule {} export class RiskInsightsRoutingModule {}

View File

@ -1,4 +1,4 @@
<div class="tw-mb-1 text-primary" bitTypography="body1">{{ "accessIntelligence" | i18n }}</div> <div class="tw-mb-1 text-primary" bitTypography="body1">{{ "riskInsights" | i18n }}</div>
<h1 bitTypography="h1">{{ "passwordRisk" | i18n }}</h1> <h1 bitTypography="h1">{{ "passwordRisk" | i18n }}</h1>
<div class="tw-text-muted">{{ "discoverAtRiskPasswords" | i18n }}</div> <div class="tw-text-muted">{{ "discoverAtRiskPasswords" | i18n }}</div>
<div class="tw-bg-primary-100 tw-rounded-lg tw-w-full tw-px-8 tw-py-2 tw-my-4"> <div class="tw-bg-primary-100 tw-rounded-lg tw-w-full tw-px-8 tw-py-2 tw-my-4">
@ -19,7 +19,7 @@
<bit-tab label="{{ 'allApplicationsWithCount' | i18n: apps.length }}"> <bit-tab label="{{ 'allApplicationsWithCount' | i18n: apps.length }}">
<tools-all-applications></tools-all-applications> <tools-all-applications></tools-all-applications>
</bit-tab> </bit-tab>
<bit-tab> <bit-tab *ngIf="isCritialAppsFeatureEnabled">
<ng-template bitTabLabel> <ng-template bitTabLabel>
<i class="bwi bwi-star"></i> <i class="bwi bwi-star"></i>
{{ "criticalApplicationsWithCount" | i18n: criticalApps.length }} {{ "criticalApplicationsWithCount" | i18n: criticalApps.length }}

View File

@ -1,9 +1,11 @@
import { CommonModule } from "@angular/common"; import { CommonModule } from "@angular/common";
import { Component } from "@angular/core"; import { Component, OnInit } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ActivatedRoute, Router } from "@angular/router"; import { ActivatedRoute, Router } from "@angular/router";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { AsyncActionsModule, ButtonModule, TabsModule } from "@bitwarden/components"; import { AsyncActionsModule, ButtonModule, TabsModule } from "@bitwarden/components";
import { HeaderModule } from "../../layouts/header/header.module"; import { HeaderModule } from "../../layouts/header/header.module";
@ -15,7 +17,7 @@ import { PasswordHealthMembersURIComponent } from "./password-health-members-uri
import { PasswordHealthMembersComponent } from "./password-health-members.component"; import { PasswordHealthMembersComponent } from "./password-health-members.component";
import { PasswordHealthComponent } from "./password-health.component"; import { PasswordHealthComponent } from "./password-health.component";
export enum AccessIntelligenceTabType { export enum RiskInsightsTabType {
AllApps = 0, AllApps = 0,
CriticalApps = 1, CriticalApps = 1,
NotifiedMembers = 2, NotifiedMembers = 2,
@ -23,7 +25,7 @@ export enum AccessIntelligenceTabType {
@Component({ @Component({
standalone: true, standalone: true,
templateUrl: "./access-intelligence.component.html", templateUrl: "./risk-insights.component.html",
imports: [ imports: [
AllApplicationsComponent, AllApplicationsComponent,
AsyncActionsModule, AsyncActionsModule,
@ -39,9 +41,10 @@ export enum AccessIntelligenceTabType {
TabsModule, TabsModule,
], ],
}) })
export class AccessIntelligenceComponent { export class RiskInsightsComponent implements OnInit {
tabIndex: AccessIntelligenceTabType; tabIndex: RiskInsightsTabType;
dataLastUpdated = new Date(); dataLastUpdated = new Date();
isCritialAppsFeatureEnabled = false;
apps: any[] = []; apps: any[] = [];
criticalApps: any[] = []; criticalApps: any[] = [];
@ -65,12 +68,19 @@ export class AccessIntelligenceComponent {
}); });
}; };
async ngOnInit() {
this.isCritialAppsFeatureEnabled = await this.configService.getFeatureFlag(
FeatureFlag.CriticalApps,
);
}
constructor( constructor(
protected route: ActivatedRoute, protected route: ActivatedRoute,
private router: Router, private router: Router,
private configService: ConfigService,
) { ) {
route.queryParams.pipe(takeUntilDestroyed()).subscribe(({ tabIndex }) => { route.queryParams.pipe(takeUntilDestroyed()).subscribe(({ tabIndex }) => {
this.tabIndex = !isNaN(tabIndex) ? tabIndex : AccessIntelligenceTabType.AllApps; this.tabIndex = !isNaN(tabIndex) ? tabIndex : RiskInsightsTabType.AllApps;
}); });
} }
} }

View File

@ -0,0 +1,9 @@
import { NgModule } from "@angular/core";
import { RiskInsightsRoutingModule } from "./risk-insights-routing.module";
import { RiskInsightsComponent } from "./risk-insights.component";
@NgModule({
imports: [RiskInsightsComponent, RiskInsightsRoutingModule],
})
export class RiskInsightsModule {}

View File

@ -7,6 +7,7 @@
*ngIf="showCipherView" *ngIf="showCipherView"
[cipher]="cipher" [cipher]="cipher"
[collections]="collections" [collections]="collections"
[isAdminConsole]="formConfig.isAdminConsole"
></app-cipher-view> ></app-cipher-view>
<vault-cipher-form <vault-cipher-form
*ngIf="loadForm" *ngIf="loadForm"

View File

@ -6,6 +6,7 @@ import { firstValueFrom, Observable, Subject } from "rxjs";
import { map } from "rxjs/operators"; import { map } from "rxjs/operators";
import { CollectionView } from "@bitwarden/admin-console/common"; import { CollectionView } from "@bitwarden/admin-console/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
@ -17,6 +18,8 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service"; import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service"; import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service";
import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
import { import {
@ -231,6 +234,7 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
private billingAccountProfileStateService: BillingAccountProfileStateService, private billingAccountProfileStateService: BillingAccountProfileStateService,
private premiumUpgradeService: PremiumUpgradePromptService, private premiumUpgradeService: PremiumUpgradePromptService,
private cipherAuthorizationService: CipherAuthorizationService, private cipherAuthorizationService: CipherAuthorizationService,
private apiService: ApiService,
) { ) {
this.updateTitle(); this.updateTitle();
} }
@ -278,7 +282,20 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
if (this._originalFormMode === "add" || this._originalFormMode === "clone") { if (this._originalFormMode === "add" || this._originalFormMode === "clone") {
this.formConfig.mode = "edit"; this.formConfig.mode = "edit";
} }
this.formConfig.originalCipher = await this.cipherService.get(cipherView.id);
let cipher: Cipher;
// When the form config is used within the Admin Console, retrieve the cipher from the admin endpoint
if (this.formConfig.isAdminConsole) {
const cipherResponse = await this.apiService.getCipherAdmin(cipherView.id);
const cipherData = new CipherData(cipherResponse);
cipher = new Cipher(cipherData);
} else {
cipher = await this.cipherService.get(cipherView.id);
}
// Store the updated cipher so any following edits use the most up to date cipher
this.formConfig.originalCipher = cipher;
this._cipherModified = true; this._cipherModified = true;
await this.changeMode("view"); await this.changeMode("view");
} }

View File

@ -16,13 +16,39 @@
"all" | i18n "all" | i18n
}}</label> }}</label>
</th> </th>
<th bitCell [class]="showExtraColumn ? 'lg:tw-w-3/5' : 'tw-w-full'">{{ "name" | i18n }}</th> <!-- Organization vault -->
<th
*ngIf="showAdminActions"
bitCell
bitSortable="name"
[fn]="sortByName"
[class]="showExtraColumn ? 'lg:tw-w-3/5' : 'tw-w-full'"
>
{{ "name" | i18n }}
</th>
<!-- Individual vault -->
<th
*ngIf="!showAdminActions"
bitCell
[class]="showExtraColumn ? 'lg:tw-w-3/5' : 'tw-w-full'"
>
{{ "name" | i18n }}
</th>
<th bitCell *ngIf="showOwner" class="tw-hidden tw-w-2/5 lg:tw-table-cell"> <th bitCell *ngIf="showOwner" class="tw-hidden tw-w-2/5 lg:tw-table-cell">
{{ "owner" | i18n }} {{ "owner" | i18n }}
</th> </th>
<th bitCell class="tw-w-2/5" *ngIf="showCollections">{{ "collections" | i18n }}</th> <th bitCell class="tw-w-2/5" *ngIf="showCollections">{{ "collections" | i18n }}</th>
<th bitCell class="tw-w-2/5" *ngIf="showGroups">{{ "groups" | i18n }}</th> <th bitCell bitSortable="groups" [fn]="sortByGroups" class="tw-w-2/5" *ngIf="showGroups">
<th bitCell class="tw-w-2/5" *ngIf="showPermissionsColumn"> {{ "groups" | i18n }}
</th>
<th
bitCell
bitSortable="permissions"
default="desc"
[fn]="sortByPermissions"
class="tw-w-2/5"
*ngIf="showPermissionsColumn"
>
{{ "permission" | i18n }} {{ "permission" | i18n }}
</th> </th>
<th bitCell class="tw-w-12 tw-text-right"> <th bitCell class="tw-w-12 tw-text-right">

View File

@ -1,13 +1,17 @@
import { SelectionModel } from "@angular/cdk/collections"; import { SelectionModel } from "@angular/cdk/collections";
import { Component, EventEmitter, Input, Output } from "@angular/core"; import { Component, EventEmitter, Input, Output } from "@angular/core";
import { CollectionView, Unassigned } from "@bitwarden/admin-console/common"; import { CollectionView, Unassigned, CollectionAdminView } from "@bitwarden/admin-console/common";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { TableDataSource } from "@bitwarden/components"; import { SortDirection, TableDataSource } from "@bitwarden/components";
import { GroupView } from "../../../admin-console/organizations/core"; import { GroupView } from "../../../admin-console/organizations/core";
import {
CollectionPermission,
convertToPermission,
} from "./../../../admin-console/organizations/shared/components/access-selector/access-selector.models";
import { VaultItem } from "./vault-item"; import { VaultItem } from "./vault-item";
import { VaultItemEvent } from "./vault-item-event"; import { VaultItemEvent } from "./vault-item-event";
@ -17,6 +21,8 @@ export const RowHeightClass = `tw-h-[65px]`;
const MaxSelectionCount = 500; const MaxSelectionCount = 500;
type ItemPermission = CollectionPermission | "NoAccess";
@Component({ @Component({
selector: "app-vault-items", selector: "app-vault-items",
templateUrl: "vault-items.component.html", templateUrl: "vault-items.component.html",
@ -333,6 +339,119 @@ export class VaultItemsComponent {
return (canEditOrManageAllCiphers || this.allCiphersHaveEditAccess()) && collectionNotSelected; return (canEditOrManageAllCiphers || this.allCiphersHaveEditAccess()) && collectionNotSelected;
} }
/**
* Sorts VaultItems, grouping collections before ciphers, and sorting each group alphabetically by name.
*/
protected sortByName = (a: VaultItem, b: VaultItem, direction: SortDirection) => {
// Collections before ciphers
const collectionCompare = this.prioritizeCollections(a, b, direction);
if (collectionCompare !== 0) {
return collectionCompare;
}
return this.compareNames(a, b);
};
/**
* Sorts VaultItems based on group names
*/
protected sortByGroups = (a: VaultItem, b: VaultItem, direction: SortDirection) => {
if (
!(a.collection instanceof CollectionAdminView) &&
!(b.collection instanceof CollectionAdminView)
) {
return 0;
}
const getFirstGroupName = (collection: CollectionAdminView): string => {
if (collection.groups.length > 0) {
return collection.groups.map((group) => this.getGroupName(group.id) || "").sort()[0];
}
return null;
};
// Collections before ciphers
const collectionCompare = this.prioritizeCollections(a, b, direction);
if (collectionCompare !== 0) {
return collectionCompare;
}
const aGroupName = getFirstGroupName(a.collection as CollectionAdminView);
const bGroupName = getFirstGroupName(b.collection as CollectionAdminView);
// Collections with groups come before collections without groups.
// If a collection has no groups, getFirstGroupName returns null.
if (aGroupName === null) {
return 1;
}
if (bGroupName === null) {
return -1;
}
return aGroupName.localeCompare(bGroupName);
};
/**
* Sorts VaultItems based on their permissions, with higher permissions taking precedence.
* If permissions are equal, it falls back to sorting by name.
*/
protected sortByPermissions = (a: VaultItem, b: VaultItem, direction: SortDirection) => {
const getPermissionPriority = (item: VaultItem): number => {
const permission = item.collection
? this.getCollectionPermission(item.collection)
: this.getCipherPermission(item.cipher);
const priorityMap = {
[CollectionPermission.Manage]: 5,
[CollectionPermission.Edit]: 4,
[CollectionPermission.EditExceptPass]: 3,
[CollectionPermission.View]: 2,
[CollectionPermission.ViewExceptPass]: 1,
NoAccess: 0,
};
return priorityMap[permission] ?? -1;
};
// Collections before ciphers
const collectionCompare = this.prioritizeCollections(a, b, direction);
if (collectionCompare !== 0) {
return collectionCompare;
}
const priorityA = getPermissionPriority(a);
const priorityB = getPermissionPriority(b);
// Higher priority first
if (priorityA !== priorityB) {
return priorityA - priorityB;
}
return this.compareNames(a, b);
};
private compareNames(a: VaultItem, b: VaultItem): number {
const getName = (item: VaultItem) => item.collection?.name || item.cipher?.name;
return getName(a).localeCompare(getName(b));
}
/**
* Sorts VaultItems by prioritizing collections over ciphers.
* Collections are always placed before ciphers, regardless of the sorting direction.
*/
private prioritizeCollections(a: VaultItem, b: VaultItem, direction: SortDirection): number {
if (a.collection && !b.collection) {
return direction === "asc" ? -1 : 1;
}
if (!a.collection && b.collection) {
return direction === "asc" ? 1 : -1;
}
return 0;
}
private hasPersonalItems(): boolean { private hasPersonalItems(): boolean {
return this.selection.selected.some(({ cipher }) => cipher?.organizationId === null); return this.selection.selected.some(({ cipher }) => cipher?.organizationId === null);
} }
@ -346,4 +465,58 @@ export class VaultItemsComponent {
private getUniqueOrganizationIds(): Set<string> { private getUniqueOrganizationIds(): Set<string> {
return new Set(this.selection.selected.flatMap((i) => i.cipher?.organizationId ?? [])); return new Set(this.selection.selected.flatMap((i) => i.cipher?.organizationId ?? []));
} }
private getGroupName(groupId: string): string | undefined {
return this.allGroups.find((g) => g.id === groupId)?.name;
}
private getCollectionPermission(collection: CollectionView): ItemPermission {
const organization = this.allOrganizations.find((o) => o.id === collection.organizationId);
if (collection.id == Unassigned && organization?.canEditUnassignedCiphers) {
return CollectionPermission.Edit;
}
if (collection.assigned) {
return convertToPermission(collection);
}
return "NoAccess";
}
private getCipherPermission(cipher: CipherView): ItemPermission {
if (!cipher.organizationId || cipher.collectionIds.length === 0) {
return CollectionPermission.Manage;
}
const filteredCollections = this.allCollections?.filter((collection) => {
if (collection.assigned) {
return cipher.collectionIds.find((id) => {
if (collection.id === id) {
return collection;
}
});
}
});
if (filteredCollections?.length === 1) {
return convertToPermission(filteredCollections[0]);
}
if (filteredCollections?.length > 0) {
const permissions = filteredCollections.map((collection) => convertToPermission(collection));
const orderedPermissions = [
CollectionPermission.Manage,
CollectionPermission.Edit,
CollectionPermission.EditExceptPass,
CollectionPermission.View,
CollectionPermission.ViewExceptPass,
];
return orderedPermissions.find((perm) => permissions.includes(perm));
}
return "NoAccess";
}
} }

View File

@ -1,5 +1,5 @@
import { Component, Inject, OnDestroy, OnInit } from "@angular/core"; import { Component, Inject, OnDestroy, OnInit } from "@angular/core";
import { combineLatest, map, Observable, Subject, takeUntil } from "rxjs"; import { combineLatest, map, Observable, of, Subject, switchMap, takeUntil } from "rxjs";
import { import {
OrganizationUserApiService, OrganizationUserApiService,
@ -8,11 +8,14 @@ import {
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@ -53,6 +56,8 @@ export class OrganizationOptionsComponent implements OnInit, OnDestroy {
private resetPasswordService: OrganizationUserResetPasswordService, private resetPasswordService: OrganizationUserResetPasswordService,
private userVerificationService: UserVerificationService, private userVerificationService: UserVerificationService,
private toastService: ToastService, private toastService: ToastService,
private configService: ConfigService,
private organizationService: OrganizationService,
) {} ) {}
async ngOnInit() { async ngOnInit() {
@ -60,23 +65,39 @@ export class OrganizationOptionsComponent implements OnInit, OnDestroy {
map((policies) => policies.filter((policy) => policy.type === PolicyType.ResetPassword)), map((policies) => policies.filter((policy) => policy.type === PolicyType.ResetPassword)),
); );
const managingOrg$ = this.configService
.getFeatureFlag$(FeatureFlag.AccountDeprovisioning)
.pipe(
switchMap((isAccountDeprovisioningEnabled) =>
isAccountDeprovisioningEnabled
? this.organizationService.organizations$.pipe(
map((organizations) =>
organizations.find((o) => o.userIsManagedByOrganization === true),
),
)
: of(null),
),
);
combineLatest([ combineLatest([
this.organization$, this.organization$,
resetPasswordPolicies$, resetPasswordPolicies$,
this.userDecryptionOptionsService.userDecryptionOptions$, this.userDecryptionOptionsService.userDecryptionOptions$,
managingOrg$,
]) ])
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
.subscribe(([organization, resetPasswordPolicies, decryptionOptions]) => { .subscribe(([organization, resetPasswordPolicies, decryptionOptions, managingOrg]) => {
this.organization = organization; this.organization = organization;
this.resetPasswordPolicy = resetPasswordPolicies.find( this.resetPasswordPolicy = resetPasswordPolicies.find(
(p) => p.organizationId === organization.id, (p) => p.organizationId === organization.id,
); );
// A user can leave an organization if they are NOT using TDE and Key Connector, or they have a master password. // A user can leave an organization if they are NOT a managed user and they are NOT using TDE and Key Connector, or they have a master password.
this.showLeaveOrgOption = this.showLeaveOrgOption =
(decryptionOptions.trustedDeviceOption == undefined && managingOrg?.id !== organization.id &&
((decryptionOptions.trustedDeviceOption == undefined &&
decryptionOptions.keyConnectorOption == undefined) || decryptionOptions.keyConnectorOption == undefined) ||
decryptionOptions.hasMasterPassword; decryptionOptions.hasMasterPassword);
// Hide the 3 dot menu if the user has no available actions // Hide the 3 dot menu if the user has no available actions
this.hideMenu = this.hideMenu =

View File

@ -1,14 +1,13 @@
import { TestBed } from "@angular/core/testing"; import { TestBed } from "@angular/core/testing";
import { BehaviorSubject } from "rxjs"; import { BehaviorSubject } from "rxjs";
import { CollectionAdminService } from "@bitwarden/admin-console/common"; import { CollectionAdminService, CollectionAdminView } from "@bitwarden/admin-console/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums"; import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { CipherId } from "@bitwarden/common/types/guid"; import { CipherId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { RoutedVaultFilterService } from "../../individual-vault/vault-filter/services/routed-vault-filter.service"; import { RoutedVaultFilterService } from "../../individual-vault/vault-filter/services/routed-vault-filter.service";
@ -35,27 +34,41 @@ describe("AdminConsoleCipherFormConfigService", () => {
status: OrganizationUserStatusType.Confirmed, status: OrganizationUserStatusType.Confirmed,
}; };
const policyAppliesToActiveUser$ = new BehaviorSubject<boolean>(true); const policyAppliesToActiveUser$ = new BehaviorSubject<boolean>(true);
const collection = {
id: "12345-5555",
organizationId: "234534-34334",
name: "Test Collection 1",
assigned: false,
readOnly: true,
} as CollectionAdminView;
const collection2 = {
id: "12345-6666",
organizationId: "22222-2222",
name: "Test Collection 2",
assigned: true,
readOnly: false,
} as CollectionAdminView;
const organization$ = new BehaviorSubject<Organization>(testOrg as Organization); const organization$ = new BehaviorSubject<Organization>(testOrg as Organization);
const organizations$ = new BehaviorSubject<Organization[]>([testOrg, testOrg2] as Organization[]); const organizations$ = new BehaviorSubject<Organization[]>([testOrg, testOrg2] as Organization[]);
const getCipherAdmin = jest.fn().mockResolvedValue(null); const getCipherAdmin = jest.fn().mockResolvedValue(null);
const getCipher = jest.fn().mockResolvedValue(null);
beforeEach(async () => { beforeEach(async () => {
getCipherAdmin.mockClear(); getCipherAdmin.mockClear();
getCipher.mockClear();
getCipher.mockResolvedValue({ id: cipherId, name: "Test Cipher - (non-admin)" });
getCipherAdmin.mockResolvedValue({ id: cipherId, name: "Test Cipher - (admin)" }); getCipherAdmin.mockResolvedValue({ id: cipherId, name: "Test Cipher - (admin)" });
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
providers: [ providers: [
AdminConsoleCipherFormConfigService, AdminConsoleCipherFormConfigService,
{ provide: OrganizationService, useValue: { get$: () => organization$, organizations$ } },
{
provide: CollectionAdminService,
useValue: { getAll: () => Promise.resolve([collection, collection2]) },
},
{ {
provide: PolicyService, provide: PolicyService,
useValue: { policyAppliesToActiveUser$: () => policyAppliesToActiveUser$ }, useValue: { policyAppliesToActiveUser$: () => policyAppliesToActiveUser$ },
}, },
{ provide: OrganizationService, useValue: { get$: () => organization$, organizations$ } },
{ provide: CipherService, useValue: { get: getCipher } },
{ provide: CollectionAdminService, useValue: { getAll: () => Promise.resolve([]) } },
{ {
provide: RoutedVaultFilterService, provide: RoutedVaultFilterService,
useValue: { filter$: new BehaviorSubject({ organizationId: testOrg.id }) }, useValue: { filter$: new BehaviorSubject({ organizationId: testOrg.id }) },
@ -86,6 +99,12 @@ describe("AdminConsoleCipherFormConfigService", () => {
expect(mode).toBe("edit"); expect(mode).toBe("edit");
}); });
it("returns all collections", async () => {
const { collections } = await adminConsoleConfigService.buildConfig("edit", cipherId);
expect(collections).toEqual([collection, collection2]);
});
it("sets admin flag based on `canEditAllCiphers`", async () => { it("sets admin flag based on `canEditAllCiphers`", async () => {
// Disable edit all ciphers on org // Disable edit all ciphers on org
testOrg.canEditAllCiphers = false; testOrg.canEditAllCiphers = false;
@ -153,23 +172,7 @@ describe("AdminConsoleCipherFormConfigService", () => {
expect(result.organizations).toEqual([testOrg, testOrg2]); expect(result.organizations).toEqual([testOrg, testOrg2]);
}); });
describe("getCipher", () => {
it("retrieves the cipher from the cipher service", async () => {
testOrg.canEditAllCiphers = false;
adminConsoleConfigService = TestBed.inject(AdminConsoleCipherFormConfigService);
const result = await adminConsoleConfigService.buildConfig("clone", cipherId);
expect(getCipher).toHaveBeenCalledWith(cipherId);
expect(result.originalCipher.name).toBe("Test Cipher - (non-admin)");
// Admin service not needed when cipher service can return the cipher
expect(getCipherAdmin).not.toHaveBeenCalled();
});
it("retrieves the cipher from the admin service", async () => { it("retrieves the cipher from the admin service", async () => {
getCipher.mockResolvedValueOnce(null);
getCipherAdmin.mockResolvedValue({ id: cipherId, name: "Test Cipher - (admin)" }); getCipherAdmin.mockResolvedValue({ id: cipherId, name: "Test Cipher - (admin)" });
adminConsoleConfigService = TestBed.inject(AdminConsoleCipherFormConfigService); adminConsoleConfigService = TestBed.inject(AdminConsoleCipherFormConfigService);
@ -177,9 +180,6 @@ describe("AdminConsoleCipherFormConfigService", () => {
await adminConsoleConfigService.buildConfig("add", cipherId); await adminConsoleConfigService.buildConfig("add", cipherId);
expect(getCipherAdmin).toHaveBeenCalledWith(cipherId); expect(getCipherAdmin).toHaveBeenCalledWith(cipherId);
expect(getCipher).toHaveBeenCalledWith(cipherId);
});
}); });
}); });
}); });

View File

@ -6,9 +6,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType, OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums"; import { PolicyType, OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { CipherId } from "@bitwarden/common/types/guid"; import { CipherId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data"; import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
@ -25,7 +23,6 @@ import { RoutedVaultFilterService } from "../../individual-vault/vault-filter/se
export class AdminConsoleCipherFormConfigService implements CipherFormConfigService { export class AdminConsoleCipherFormConfigService implements CipherFormConfigService {
private policyService: PolicyService = inject(PolicyService); private policyService: PolicyService = inject(PolicyService);
private organizationService: OrganizationService = inject(OrganizationService); private organizationService: OrganizationService = inject(OrganizationService);
private cipherService: CipherService = inject(CipherService);
private routedVaultFilterService: RoutedVaultFilterService = inject(RoutedVaultFilterService); private routedVaultFilterService: RoutedVaultFilterService = inject(RoutedVaultFilterService);
private collectionAdminService: CollectionAdminService = inject(CollectionAdminService); private collectionAdminService: CollectionAdminService = inject(CollectionAdminService);
private apiService: ApiService = inject(ApiService); private apiService: ApiService = inject(ApiService);
@ -51,20 +48,8 @@ export class AdminConsoleCipherFormConfigService implements CipherFormConfigServ
map(([orgs, orgId]) => orgs.find((o) => o.id === orgId)), map(([orgs, orgId]) => orgs.find((o) => o.id === orgId)),
); );
private editableCollections$ = this.organization$.pipe( private allCollections$ = this.organization$.pipe(
switchMap(async (org) => { switchMap(async (org) => await this.collectionAdminService.getAll(org.id)),
if (!org) {
return [];
}
const collections = await this.collectionAdminService.getAll(org.id);
// Users that can edit all ciphers can implicitly add to / edit within any collection
if (org.canEditAllCiphers) {
return collections;
}
// The user is only allowed to add/edit items to assigned collections that are not readonly
return collections.filter((c) => c.assigned && !c.readOnly);
}),
); );
async buildConfig( async buildConfig(
@ -72,21 +57,17 @@ export class AdminConsoleCipherFormConfigService implements CipherFormConfigServ
cipherId?: CipherId, cipherId?: CipherId,
cipherType?: CipherType, cipherType?: CipherType,
): Promise<CipherFormConfig> { ): Promise<CipherFormConfig> {
const cipher = await this.getCipher(cipherId);
const [organization, allowPersonalOwnership, allOrganizations, allCollections] = const [organization, allowPersonalOwnership, allOrganizations, allCollections] =
await firstValueFrom( await firstValueFrom(
combineLatest([ combineLatest([
this.organization$, this.organization$,
this.allowPersonalOwnership$, this.allowPersonalOwnership$,
this.allOrganizations$, this.allOrganizations$,
this.editableCollections$, this.allCollections$,
]), ]),
); );
const cipher = await this.getCipher(organization, cipherId);
const collections = allCollections.filter(
(c) => c.organizationId === organization.id && c.assigned && !c.readOnly,
);
// When cloning from within the Admin Console, all organizations should be available. // When cloning from within the Admin Console, all organizations should be available.
// Otherwise only the one in context should be // Otherwise only the one in context should be
const organizations = mode === "clone" ? allOrganizations : [organization]; const organizations = mode === "clone" ? allOrganizations : [organization];
@ -100,7 +81,7 @@ export class AdminConsoleCipherFormConfigService implements CipherFormConfigServ
admin: organization.canEditAllCiphers ?? false, admin: organization.canEditAllCiphers ?? false,
allowPersonalOwnership: allowPersonalOwnershipOnlyForClone, allowPersonalOwnership: allowPersonalOwnershipOnlyForClone,
originalCipher: cipher, originalCipher: cipher,
collections, collections: allCollections,
organizations, organizations,
folders: [], // folders not applicable in the admin console folders: [], // folders not applicable in the admin console
hideIndividualVaultFields: true, hideIndividualVaultFields: true,
@ -108,19 +89,11 @@ export class AdminConsoleCipherFormConfigService implements CipherFormConfigServ
}; };
} }
private async getCipher(organization: Organization, id?: CipherId): Promise<Cipher | null> { private async getCipher(id?: CipherId): Promise<Cipher | null> {
if (id == null) { if (id == null) {
return Promise.resolve(null); return Promise.resolve(null);
} }
// Check to see if the user has direct access to the cipher
const cipherFromCipherService = await this.cipherService.get(id);
// If the organization doesn't allow admin/owners to edit all ciphers return the cipher
if (!organization.canEditAllCiphers && cipherFromCipherService != null) {
return cipherFromCipherService;
}
// Retrieve the cipher through the means of an admin // Retrieve the cipher through the means of an admin
const cipherResponse = await this.apiService.getCipherAdmin(id); const cipherResponse = await this.apiService.getCipherAdmin(id);
cipherResponse.edit = true; cipherResponse.edit = true;

View File

@ -5,8 +5,8 @@
"criticalApplications": { "criticalApplications": {
"message": "Critical applications" "message": "Critical applications"
}, },
"accessIntelligence": { "riskInsights": {
"message": "Access Intelligence" "message": "Risk Insights"
}, },
"passwordRisk": { "passwordRisk": {
"message": "Password Risk" "message": "Password Risk"

View File

@ -1,9 +1,9 @@
import { Inject, Injectable } from "@angular/core"; import { Inject, Injectable } from "@angular/core";
// eslint-disable-next-line no-restricted-imports // eslint-disable-next-line no-restricted-imports
import { mockCiphers } from "@bitwarden/bit-common/tools/reports/access-intelligence/services/ciphers.mock"; import { mockCiphers } from "@bitwarden/bit-common/tools/reports/risk-insights/services/ciphers.mock";
// eslint-disable-next-line no-restricted-imports // eslint-disable-next-line no-restricted-imports
import { mockMemberCipherDetailsResponse } from "@bitwarden/bit-common/tools/reports/access-intelligence/services/member-cipher-details-response.mock"; import { mockMemberCipherDetailsResponse } from "@bitwarden/bit-common/tools/reports/risk-insights/services/member-cipher-details-response.mock";
import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";

View File

@ -41,6 +41,7 @@ module.exports = {
"<rootDir>/libs/platform/jest.config.js", "<rootDir>/libs/platform/jest.config.js",
"<rootDir>/libs/node/jest.config.js", "<rootDir>/libs/node/jest.config.js",
"<rootDir>/libs/vault/jest.config.js", "<rootDir>/libs/vault/jest.config.js",
"<rootDir>/libs/key-management/jest.config.js",
], ],
// Workaround for a memory leak that crashes tests in CI: // Workaround for a memory leak that crashes tests in CI:

View File

@ -38,18 +38,20 @@
<div class="box-content"> <div class="box-content">
<div <div
class="environment-selector-dialog" class="environment-selector-dialog"
data-testid="environment-selector-dialog"
[@transformPanel]="'open'" [@transformPanel]="'open'"
cdkTrapFocus cdkTrapFocus
cdkTrapFocusAutoCapture cdkTrapFocusAutoCapture
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
> >
<ng-container *ngFor="let region of availableRegions"> <ng-container *ngFor="let region of availableRegions; let i = index">
<button <button
type="button" type="button"
class="environment-selector-dialog-item" class="environment-selector-dialog-item"
(click)="toggle(region.key)" (click)="toggle(region.key)"
[attr.aria-pressed]="data.selectedRegion === region ? 'true' : 'false'" [attr.aria-pressed]="data.selectedRegion === region ? 'true' : 'false'"
[attr.data-testid]="'environment-selector-dialog-item-' + i"
> >
<i <i
class="bwi bwi-fw bwi-sm bwi-check" class="bwi bwi-fw bwi-sm bwi-check"
@ -66,6 +68,7 @@
class="environment-selector-dialog-item" class="environment-selector-dialog-item"
(click)="toggle(ServerEnvironmentType.SelfHosted)" (click)="toggle(ServerEnvironmentType.SelfHosted)"
[attr.aria-pressed]="data.selectedRegion ? 'false' : 'true'" [attr.aria-pressed]="data.selectedRegion ? 'false' : 'true'"
data-testid="environment-selector-dialog-item-self-hosted"
> >
<i <i
class="bwi bwi-fw bwi-sm bwi-check" class="bwi bwi-fw bwi-sm bwi-check"

View File

@ -178,6 +178,7 @@ import { BulkEncryptServiceImplementation } from "@bitwarden/common/platform/ser
import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation"; import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation";
import { DefaultBroadcasterService } from "@bitwarden/common/platform/services/default-broadcaster.service"; import { DefaultBroadcasterService } from "@bitwarden/common/platform/services/default-broadcaster.service";
import { DefaultEnvironmentService } from "@bitwarden/common/platform/services/default-environment.service"; import { DefaultEnvironmentService } from "@bitwarden/common/platform/services/default-environment.service";
import { DefaultServerSettingsService } from "@bitwarden/common/platform/services/default-server-settings.service";
import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service"; import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service";
import { KeyGenerationService } from "@bitwarden/common/platform/services/key-generation.service"; import { KeyGenerationService } from "@bitwarden/common/platform/services/key-generation.service";
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service"; import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
@ -1322,6 +1323,11 @@ const safeProviders: SafeProvider[] = [
InternalUserDecryptionOptionsServiceAbstraction, InternalUserDecryptionOptionsServiceAbstraction,
], ],
}), }),
safeProvider({
provide: DefaultServerSettingsService,
useClass: DefaultServerSettingsService,
deps: [ConfigService],
}),
safeProvider({ safeProvider({
provide: RegisterRouteService, provide: RegisterRouteService,
useClass: RegisterRouteService, useClass: RegisterRouteService,

View File

@ -699,7 +699,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
} }
protected deleteCipher() { protected deleteCipher() {
const asAdmin = this.organization?.canEditAllCiphers; const asAdmin = this.organization?.canEditAllCiphers || !this.cipher.collectionIds;
return this.cipher.isDeleted return this.cipher.isDeleted
? this.cipherService.deleteWithServer(this.cipher.id, asAdmin) ? this.cipherService.deleteWithServer(this.cipher.id, asAdmin)
: this.cipherService.softDeleteWithServer(this.cipher.id, asAdmin); : this.cipherService.softDeleteWithServer(this.cipher.id, asAdmin);

View File

@ -4,13 +4,14 @@ import { RouterModule } from "@angular/router";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
import { RegisterRouteService } from "@bitwarden/auth/common"; import { RegisterRouteService } from "@bitwarden/auth/common";
import { DefaultServerSettingsService } from "@bitwarden/common/platform/services/default-server-settings.service";
import { LinkModule } from "@bitwarden/components"; import { LinkModule } from "@bitwarden/components";
@Component({ @Component({
standalone: true, standalone: true,
imports: [CommonModule, JslibModule, LinkModule, RouterModule], imports: [CommonModule, JslibModule, LinkModule, RouterModule],
template: ` template: `
<div class="tw-text-center"> <div class="tw-text-center" *ngIf="!(isUserRegistrationDisabled$ | async)">
{{ "newToBitwarden" | i18n }} {{ "newToBitwarden" | i18n }}
<a bitLink [routerLink]="registerRoute$ | async">{{ "createAccount" | i18n }}</a> <a bitLink [routerLink]="registerRoute$ | async">{{ "createAccount" | i18n }}</a>
</div> </div>
@ -18,7 +19,10 @@ import { LinkModule } from "@bitwarden/components";
}) })
export class LoginSecondaryContentComponent { export class LoginSecondaryContentComponent {
registerRouteService = inject(RegisterRouteService); registerRouteService = inject(RegisterRouteService);
serverSettingsService = inject(DefaultServerSettingsService);
// TODO: remove when email verification flag is removed // TODO: remove when email verification flag is removed
protected registerRoute$ = this.registerRouteService.registerRoute$(); protected registerRoute$ = this.registerRouteService.registerRoute$();
protected isUserRegistrationDisabled$ = this.serverSettingsService.isUserRegistrationDisabled$;
} }

View File

@ -11,7 +11,7 @@
--> -->
<form [bitSubmit]="submit" [formGroup]="formGroup"> <form [bitSubmit]="submit" [formGroup]="formGroup">
<ng-container *ngIf="loginUiState === LoginUiState.EMAIL_ENTRY"> <div [ngClass]="{ 'tw-invisible tw-h-0': loginUiState !== LoginUiState.EMAIL_ENTRY }">
<!-- Email Address input --> <!-- Email Address input -->
<bit-form-field> <bit-form-field>
<bit-label>{{ "emailAddress" | i18n }}</bit-label> <bit-label>{{ "emailAddress" | i18n }}</bit-label>
@ -82,9 +82,9 @@
</button> </button>
</ng-container> </ng-container>
</div> </div>
</ng-container> </div>
<ng-container *ngIf="loginUiState === LoginUiState.MASTER_PASSWORD_ENTRY"> <div [ngClass]="{ 'tw-invisible tw-h-0': loginUiState !== LoginUiState.MASTER_PASSWORD_ENTRY }">
<!-- Master Password input --> <!-- Master Password input -->
<bit-form-field class="!tw-mb-1"> <bit-form-field class="!tw-mb-1">
<bit-label>{{ "masterPass" | i18n }}</bit-label> <bit-label>{{ "masterPass" | i18n }}</bit-label>
@ -140,5 +140,5 @@
</button> </button>
</ng-container> </ng-container>
</div> </div>
</ng-container> </div>
</form> </form>

View File

@ -2,7 +2,7 @@ import { CommonModule } from "@angular/common";
import { Component, ElementRef, Input, NgZone, OnDestroy, OnInit, ViewChild } from "@angular/core"; import { Component, ElementRef, Input, NgZone, OnDestroy, OnInit, ViewChild } from "@angular/core";
import { FormBuilder, FormControl, ReactiveFormsModule, Validators } from "@angular/forms"; import { FormBuilder, FormControl, ReactiveFormsModule, Validators } from "@angular/forms";
import { ActivatedRoute, Router, RouterModule } from "@angular/router"; import { ActivatedRoute, Router, RouterModule } from "@angular/router";
import { firstValueFrom, Subject, take, takeUntil } from "rxjs"; import { firstValueFrom, Subject, take, takeUntil, tap } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
import { import {
@ -19,9 +19,11 @@ import { CaptchaIFrame } from "@bitwarden/common/auth/captcha-iframe";
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { ClientType, HttpStatusCode } from "@bitwarden/common/enums"; import { ClientType, HttpStatusCode } from "@bitwarden/common/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@ -139,12 +141,16 @@ export class LoginComponent implements OnInit, OnDestroy {
private toastService: ToastService, private toastService: ToastService,
private logService: LogService, private logService: LogService,
private validationService: ValidationService, private validationService: ValidationService,
private configService: ConfigService,
) { ) {
this.clientType = this.platformUtilsService.getClientType(); this.clientType = this.platformUtilsService.getClientType();
this.loginViaAuthRequestSupported = this.loginComponentService.isLoginViaAuthRequestSupported(); this.loginViaAuthRequestSupported = this.loginComponentService.isLoginViaAuthRequestSupported();
} }
async ngOnInit(): Promise<void> { async ngOnInit(): Promise<void> {
// TODO: remove this when the UnauthenticatedExtensionUIRefresh feature flag is removed.
this.listenForUnauthUiRefreshFlagChanges();
await this.defaultOnInit(); await this.defaultOnInit();
if (this.clientType === ClientType.Desktop) { if (this.clientType === ClientType.Desktop) {
@ -162,6 +168,29 @@ export class LoginComponent implements OnInit, OnDestroy {
this.destroy$.complete(); this.destroy$.complete();
} }
private listenForUnauthUiRefreshFlagChanges() {
this.configService
.getFeatureFlag$(FeatureFlag.UnauthenticatedExtensionUIRefresh)
.pipe(
tap(async (flag) => {
// If the flag is turned OFF, we must force a reload to ensure the correct UI is shown
if (!flag) {
const uniqueQueryParams = {
...this.activatedRoute.queryParams,
// adding a unique timestamp to the query params to force a reload
t: new Date().getTime().toString(), // Adding a unique timestamp as a query parameter
};
await this.router.navigate(["/"], {
queryParams: uniqueQueryParams,
});
}
}),
takeUntil(this.destroy$),
)
.subscribe();
}
submit = async (): Promise<void> => { submit = async (): Promise<void> => {
if (this.clientType === ClientType.Desktop) { if (this.clientType === ClientType.Desktop) {
if (this.loginUiState !== LoginUiState.MASTER_PASSWORD_ENTRY) { if (this.loginUiState !== LoginUiState.MASTER_PASSWORD_ENTRY) {

View File

@ -1,7 +1,7 @@
<form [formGroup]="formGroup" *ngIf="!hideEnvSelector"> <form [formGroup]="formGroup" *ngIf="!hideEnvSelector">
<bit-form-field> <bit-form-field>
<bit-label>{{ "creatingAccountOn" | i18n }}</bit-label> <bit-label>{{ "creatingAccountOn" | i18n }}</bit-label>
<bit-select formControlName="selectedRegion"> <bit-select formControlName="selectedRegion" (closed)="onSelectClosed()">
<bit-option <bit-option
*ngFor="let regionConfig of availableRegionConfigs" *ngFor="let regionConfig of availableRegionConfigs"
[value]="regionConfig" [value]="regionConfig"

View File

@ -109,6 +109,9 @@ export class RegistrationEnvSelectorComponent implements OnInit, OnDestroy {
.subscribe(); .subscribe();
} }
/**
* Listens for changes to the selected region and updates the form value and emits the selected region.
*/
private listenForSelectedRegionChanges() { private listenForSelectedRegionChanges() {
this.selectedRegion.valueChanges this.selectedRegion.valueChanges
.pipe( .pipe(
@ -124,16 +127,12 @@ export class RegistrationEnvSelectorComponent implements OnInit, OnDestroy {
return of(null); return of(null);
} }
if (selectedRegion === Region.SelfHosted) { if (selectedRegion !== Region.SelfHosted) {
return from(SelfHostedEnvConfigDialogComponent.open(this.dialogService)).pipe(
tap((result: boolean | undefined) =>
this.handleSelfHostedEnvConfigDialogResult(result, prevSelectedRegion),
),
);
}
this.selectedRegionChange.emit(selectedRegion); this.selectedRegionChange.emit(selectedRegion);
return from(this.environmentService.setEnvironment(selectedRegion.key)); return from(this.environmentService.setEnvironment(selectedRegion.key));
}
return of(null);
}, },
), ),
takeUntil(this.destroy$), takeUntil(this.destroy$),
@ -170,6 +169,17 @@ export class RegistrationEnvSelectorComponent implements OnInit, OnDestroy {
} }
} }
/**
* Handles the event when the select is closed.
* If the selected region is self-hosted, opens the self-hosted environment settings dialog.
*/
protected async onSelectClosed() {
if (this.selectedRegion.value === Region.SelfHosted) {
const result = await SelfHostedEnvConfigDialogComponent.open(this.dialogService);
return this.handleSelfHostedEnvConfigDialogResult(result, this.selectedRegion.value);
}
}
ngOnDestroy() { ngOnDestroy() {
this.destroy$.next(); this.destroy$.next();
this.destroy$.complete(); this.destroy$.complete();

View File

@ -17,7 +17,6 @@ export enum FeatureFlag {
InlineMenuFieldQualification = "inline-menu-field-qualification", InlineMenuFieldQualification = "inline-menu-field-qualification",
MemberAccessReport = "ac-2059-member-access-report", MemberAccessReport = "ac-2059-member-access-report",
TwoFactorComponentRefactor = "two-factor-component-refactor", TwoFactorComponentRefactor = "two-factor-component-refactor",
EnableTimeThreshold = "PM-5864-dollar-threshold",
InlineMenuPositioningImprovements = "inline-menu-positioning-improvements", InlineMenuPositioningImprovements = "inline-menu-positioning-improvements",
ProviderClientVaultPrivacyBanner = "ac-2833-provider-client-vault-privacy-banner", ProviderClientVaultPrivacyBanner = "ac-2833-provider-client-vault-privacy-banner",
VaultBulkManagementAction = "vault-bulk-management-action", VaultBulkManagementAction = "vault-bulk-management-action",
@ -36,6 +35,7 @@ export enum FeatureFlag {
AccessIntelligence = "pm-13227-access-intelligence", AccessIntelligence = "pm-13227-access-intelligence",
Pm13322AddPolicyDefinitions = "pm-13322-add-policy-definitions", Pm13322AddPolicyDefinitions = "pm-13322-add-policy-definitions",
LimitCollectionCreationDeletionSplit = "pm-10863-limit-collection-creation-deletion-split", LimitCollectionCreationDeletionSplit = "pm-10863-limit-collection-creation-deletion-split",
CriticalApps = "pm-14466-risk-insights-critical-application",
DisableFreeFamiliesSponsorship = "PM-12274-disable-free-families-sponsorship", DisableFreeFamiliesSponsorship = "PM-12274-disable-free-families-sponsorship",
} }
@ -64,7 +64,6 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.InlineMenuFieldQualification]: FALSE, [FeatureFlag.InlineMenuFieldQualification]: FALSE,
[FeatureFlag.MemberAccessReport]: FALSE, [FeatureFlag.MemberAccessReport]: FALSE,
[FeatureFlag.TwoFactorComponentRefactor]: FALSE, [FeatureFlag.TwoFactorComponentRefactor]: FALSE,
[FeatureFlag.EnableTimeThreshold]: FALSE,
[FeatureFlag.InlineMenuPositioningImprovements]: FALSE, [FeatureFlag.InlineMenuPositioningImprovements]: FALSE,
[FeatureFlag.ProviderClientVaultPrivacyBanner]: FALSE, [FeatureFlag.ProviderClientVaultPrivacyBanner]: FALSE,
[FeatureFlag.VaultBulkManagementAction]: FALSE, [FeatureFlag.VaultBulkManagementAction]: FALSE,
@ -83,6 +82,7 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.AccessIntelligence]: FALSE, [FeatureFlag.AccessIntelligence]: FALSE,
[FeatureFlag.Pm13322AddPolicyDefinitions]: FALSE, [FeatureFlag.Pm13322AddPolicyDefinitions]: FALSE,
[FeatureFlag.LimitCollectionCreationDeletionSplit]: FALSE, [FeatureFlag.LimitCollectionCreationDeletionSplit]: FALSE,
[FeatureFlag.CriticalApps]: FALSE,
[FeatureFlag.DisableFreeFamiliesSponsorship]: FALSE, [FeatureFlag.DisableFreeFamiliesSponsorship]: FALSE,
} satisfies Record<FeatureFlag, AllowedFeatureFlagTypes>; } satisfies Record<FeatureFlag, AllowedFeatureFlagTypes>;

View File

@ -3,6 +3,7 @@ import { SemVer } from "semver";
import { FeatureFlag, FeatureFlagValueType } from "../../../enums/feature-flag.enum"; import { FeatureFlag, FeatureFlagValueType } from "../../../enums/feature-flag.enum";
import { UserId } from "../../../types/guid"; import { UserId } from "../../../types/guid";
import { ServerSettings } from "../../models/domain/server-settings";
import { Region } from "../environment.service"; import { Region } from "../environment.service";
import { ServerConfig } from "./server-config"; import { ServerConfig } from "./server-config";
@ -10,6 +11,8 @@ import { ServerConfig } from "./server-config";
export abstract class ConfigService { export abstract class ConfigService {
/** The server config of the currently active user */ /** The server config of the currently active user */
serverConfig$: Observable<ServerConfig | null>; serverConfig$: Observable<ServerConfig | null>;
/** The server settings of the currently active user */
serverSettings$: Observable<ServerSettings | null>;
/** The cloud region of the currently active user */ /** The cloud region of the currently active user */
cloudRegion$: Observable<Region>; cloudRegion$: Observable<Region>;
/** /**

View File

@ -6,6 +6,7 @@ import {
ThirdPartyServerConfigData, ThirdPartyServerConfigData,
EnvironmentServerConfigData, EnvironmentServerConfigData,
} from "../../models/data/server-config.data"; } from "../../models/data/server-config.data";
import { ServerSettings } from "../../models/domain/server-settings";
const dayInMilliseconds = 24 * 3600 * 1000; const dayInMilliseconds = 24 * 3600 * 1000;
@ -16,6 +17,7 @@ export class ServerConfig {
environment?: EnvironmentServerConfigData; environment?: EnvironmentServerConfigData;
utcDate: Date; utcDate: Date;
featureStates: { [key: string]: AllowedFeatureFlagTypes } = {}; featureStates: { [key: string]: AllowedFeatureFlagTypes } = {};
settings: ServerSettings;
constructor(serverConfigData: ServerConfigData) { constructor(serverConfigData: ServerConfigData) {
this.version = serverConfigData.version; this.version = serverConfigData.version;
@ -24,6 +26,7 @@ export class ServerConfig {
this.utcDate = new Date(serverConfigData.utcDate); this.utcDate = new Date(serverConfigData.utcDate);
this.environment = serverConfigData.environment; this.environment = serverConfigData.environment;
this.featureStates = serverConfigData.featureStates; this.featureStates = serverConfigData.featureStates;
this.settings = serverConfigData.settings;
if (this.server?.name == null && this.server?.url == null) { if (this.server?.name == null && this.server?.url == null) {
this.server = null; this.server = null;

View File

@ -3,6 +3,7 @@
export type SharedFlags = { export type SharedFlags = {
showPasswordless?: boolean; showPasswordless?: boolean;
sdk?: boolean; sdk?: boolean;
prereleaseBuild?: boolean;
}; };
// required to avoid linting errors when there are no flags // required to avoid linting errors when there are no flags

View File

@ -16,6 +16,9 @@ describe("ServerConfigData", () => {
name: "test", name: "test",
url: "https://test.com", url: "https://test.com",
}, },
settings: {
disableUserRegistration: false,
},
environment: { environment: {
cloudRegion: Region.EU, cloudRegion: Region.EU,
vault: "https://vault.com", vault: "https://vault.com",

View File

@ -2,6 +2,7 @@ import { Jsonify } from "type-fest";
import { AllowedFeatureFlagTypes } from "../../../enums/feature-flag.enum"; import { AllowedFeatureFlagTypes } from "../../../enums/feature-flag.enum";
import { Region } from "../../abstractions/environment.service"; import { Region } from "../../abstractions/environment.service";
import { ServerSettings } from "../domain/server-settings";
import { import {
ServerConfigResponse, ServerConfigResponse,
ThirdPartyServerConfigResponse, ThirdPartyServerConfigResponse,
@ -15,6 +16,7 @@ export class ServerConfigData {
environment?: EnvironmentServerConfigData; environment?: EnvironmentServerConfigData;
utcDate: string; utcDate: string;
featureStates: { [key: string]: AllowedFeatureFlagTypes } = {}; featureStates: { [key: string]: AllowedFeatureFlagTypes } = {};
settings: ServerSettings;
constructor(serverConfigResponse: Partial<ServerConfigResponse>) { constructor(serverConfigResponse: Partial<ServerConfigResponse>) {
this.version = serverConfigResponse?.version; this.version = serverConfigResponse?.version;
@ -27,6 +29,7 @@ export class ServerConfigData {
? new EnvironmentServerConfigData(serverConfigResponse.environment) ? new EnvironmentServerConfigData(serverConfigResponse.environment)
: null; : null;
this.featureStates = serverConfigResponse?.featureStates; this.featureStates = serverConfigResponse?.featureStates;
this.settings = new ServerSettings(serverConfigResponse.settings);
} }
static fromJSON(obj: Jsonify<ServerConfigData>): ServerConfigData { static fromJSON(obj: Jsonify<ServerConfigData>): ServerConfigData {

View File

@ -0,0 +1,20 @@
import { ServerSettings } from "./server-settings";
describe("ServerSettings", () => {
describe("disableUserRegistration", () => {
it("defaults disableUserRegistration to false", () => {
const settings = new ServerSettings();
expect(settings.disableUserRegistration).toBe(false);
});
it("sets disableUserRegistration to true when provided", () => {
const settings = new ServerSettings({ disableUserRegistration: true });
expect(settings.disableUserRegistration).toBe(true);
});
it("sets disableUserRegistration to false when provided", () => {
const settings = new ServerSettings({ disableUserRegistration: false });
expect(settings.disableUserRegistration).toBe(false);
});
});
});

View File

@ -0,0 +1,7 @@
export class ServerSettings {
disableUserRegistration: boolean;
constructor(data?: ServerSettings) {
this.disableUserRegistration = data?.disableUserRegistration ?? false;
}
}

View File

@ -1,6 +1,7 @@
import { AllowedFeatureFlagTypes } from "../../../enums/feature-flag.enum"; import { AllowedFeatureFlagTypes } from "../../../enums/feature-flag.enum";
import { BaseResponse } from "../../../models/response/base.response"; import { BaseResponse } from "../../../models/response/base.response";
import { Region } from "../../abstractions/environment.service"; import { Region } from "../../abstractions/environment.service";
import { ServerSettings } from "../domain/server-settings";
export class ServerConfigResponse extends BaseResponse { export class ServerConfigResponse extends BaseResponse {
version: string; version: string;
@ -8,6 +9,7 @@ export class ServerConfigResponse extends BaseResponse {
server: ThirdPartyServerConfigResponse; server: ThirdPartyServerConfigResponse;
environment: EnvironmentServerConfigResponse; environment: EnvironmentServerConfigResponse;
featureStates: { [key: string]: AllowedFeatureFlagTypes } = {}; featureStates: { [key: string]: AllowedFeatureFlagTypes } = {};
settings: ServerSettings;
constructor(response: any) { constructor(response: any) {
super(response); super(response);
@ -21,6 +23,7 @@ export class ServerConfigResponse extends BaseResponse {
this.server = new ThirdPartyServerConfigResponse(this.getResponseProperty("Server")); this.server = new ThirdPartyServerConfigResponse(this.getResponseProperty("Server"));
this.environment = new EnvironmentServerConfigResponse(this.getResponseProperty("Environment")); this.environment = new EnvironmentServerConfigResponse(this.getResponseProperty("Environment"));
this.featureStates = this.getResponseProperty("FeatureStates"); this.featureStates = this.getResponseProperty("FeatureStates");
this.settings = new ServerSettings(this.getResponseProperty("Settings"));
} }
} }

View File

@ -28,6 +28,7 @@ import { Environment, EnvironmentService, Region } from "../../abstractions/envi
import { LogService } from "../../abstractions/log.service"; import { LogService } from "../../abstractions/log.service";
import { devFlagEnabled, devFlagValue } from "../../misc/flags"; import { devFlagEnabled, devFlagValue } from "../../misc/flags";
import { ServerConfigData } from "../../models/data/server-config.data"; import { ServerConfigData } from "../../models/data/server-config.data";
import { ServerSettings } from "../../models/domain/server-settings";
import { CONFIG_DISK, KeyDefinition, StateProvider, UserKeyDefinition } from "../../state"; import { CONFIG_DISK, KeyDefinition, StateProvider, UserKeyDefinition } from "../../state";
export const RETRIEVAL_INTERVAL = devFlagEnabled("configRetrievalIntervalMs") export const RETRIEVAL_INTERVAL = devFlagEnabled("configRetrievalIntervalMs")
@ -57,6 +58,8 @@ export class DefaultConfigService implements ConfigService {
serverConfig$: Observable<ServerConfig>; serverConfig$: Observable<ServerConfig>;
serverSettings$: Observable<ServerSettings>;
cloudRegion$: Observable<Region>; cloudRegion$: Observable<Region>;
constructor( constructor(
@ -111,6 +114,10 @@ export class DefaultConfigService implements ConfigService {
this.cloudRegion$ = this.serverConfig$.pipe( this.cloudRegion$ = this.serverConfig$.pipe(
map((config) => config?.environment?.cloudRegion ?? Region.US), map((config) => config?.environment?.cloudRegion ?? Region.US),
); );
this.serverSettings$ = this.serverConfig$.pipe(
map((config) => config?.settings ?? new ServerSettings()),
);
} }
getFeatureFlag$<Flag extends FeatureFlag>(key: Flag) { getFeatureFlag$<Flag extends FeatureFlag>(key: Flag) {

View File

@ -0,0 +1,47 @@
import { of } from "rxjs";
import { ConfigService } from "../abstractions/config/config.service";
import { ServerSettings } from "../models/domain/server-settings";
import { DefaultServerSettingsService } from "./default-server-settings.service";
describe("DefaultServerSettingsService", () => {
let service: DefaultServerSettingsService;
let configServiceMock: { serverSettings$: any };
beforeEach(() => {
configServiceMock = { serverSettings$: of() };
service = new DefaultServerSettingsService(configServiceMock as ConfigService);
});
describe("getSettings$", () => {
it("returns server settings", () => {
const mockSettings = new ServerSettings({ disableUserRegistration: true });
configServiceMock.serverSettings$ = of(mockSettings);
service.getSettings$().subscribe((settings) => {
expect(settings).toEqual(mockSettings);
});
});
});
describe("isUserRegistrationDisabled$", () => {
it("returns true when user registration is disabled", () => {
const mockSettings = new ServerSettings({ disableUserRegistration: true });
configServiceMock.serverSettings$ = of(mockSettings);
service.isUserRegistrationDisabled$.subscribe((isDisabled: boolean) => {
expect(isDisabled).toBe(true);
});
});
it("returns false when user registration is enabled", () => {
const mockSettings = new ServerSettings({ disableUserRegistration: false });
configServiceMock.serverSettings$ = of(mockSettings);
service.isUserRegistrationDisabled$.subscribe((isDisabled: boolean) => {
expect(isDisabled).toBe(false);
});
});
});
});

View File

@ -0,0 +1,19 @@
import { Observable } from "rxjs";
import { map } from "rxjs/operators";
import { ConfigService } from "../abstractions/config/config.service";
import { ServerSettings } from "../models/domain/server-settings";
export class DefaultServerSettingsService {
constructor(private configService: ConfigService) {}
getSettings$(): Observable<ServerSettings> {
return this.configService.serverSettings$;
}
get isUserRegistrationDisabled$(): Observable<boolean> {
return this.getSettings$().pipe(
map((settings: ServerSettings) => settings.disableUserRegistration),
);
}
}

View File

@ -126,6 +126,7 @@ import { AppIdService } from "../platform/abstractions/app-id.service";
import { EnvironmentService } from "../platform/abstractions/environment.service"; import { EnvironmentService } from "../platform/abstractions/environment.service";
import { LogService } from "../platform/abstractions/log.service"; import { LogService } from "../platform/abstractions/log.service";
import { PlatformUtilsService } from "../platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "../platform/abstractions/platform-utils.service";
import { flagEnabled } from "../platform/misc/flags";
import { Utils } from "../platform/misc/utils"; import { Utils } from "../platform/misc/utils";
import { SyncResponse } from "../platform/sync"; import { SyncResponse } from "../platform/sync";
import { UserId } from "../types/guid"; import { UserId } from "../types/guid";
@ -583,7 +584,7 @@ export class ApiService implements ApiServiceAbstraction {
} }
putCipherCollectionsAdmin(id: string, request: CipherCollectionsRequest): Promise<any> { putCipherCollectionsAdmin(id: string, request: CipherCollectionsRequest): Promise<any> {
return this.send("PUT", "/ciphers/" + id + "/collections-admin", request, true, false); return this.send("PUT", "/ciphers/" + id + "/collections-admin", request, true, true);
} }
postPurgeCiphers( postPurgeCiphers(
@ -1843,44 +1844,20 @@ export class ApiService implements ApiServiceAbstraction {
const requestUrl = const requestUrl =
apiUrl + Utils.normalizePath(pathParts[0]) + (pathParts.length > 1 ? `?${pathParts[1]}` : ""); apiUrl + Utils.normalizePath(pathParts[0]) + (pathParts.length > 1 ? `?${pathParts[1]}` : "");
const headers = new Headers({ const [requestHeaders, requestBody] = await this.buildHeadersAndBody(
"Device-Type": this.deviceType, authed,
}); hasResponse,
if (this.customUserAgent != null) { body,
headers.set("User-Agent", this.customUserAgent); alterHeaders,
} );
const requestInit: RequestInit = { const requestInit: RequestInit = {
cache: "no-store", cache: "no-store",
credentials: await this.getCredentials(), credentials: await this.getCredentials(),
method: method, method: method,
}; };
requestInit.headers = requestHeaders;
if (authed) { requestInit.body = requestBody;
const authHeader = await this.getActiveBearerToken();
headers.set("Authorization", "Bearer " + authHeader);
}
if (body != null) {
if (typeof body === "string") {
requestInit.body = body;
headers.set("Content-Type", "application/x-www-form-urlencoded; charset=utf-8");
} else if (typeof body === "object") {
if (body instanceof FormData) {
requestInit.body = body;
} else {
headers.set("Content-Type", "application/json; charset=utf-8");
requestInit.body = JSON.stringify(body);
}
}
}
if (hasResponse) {
headers.set("Accept", "application/json");
}
if (alterHeaders != null) {
alterHeaders(headers);
}
requestInit.headers = headers;
const response = await this.fetch(new Request(requestUrl, requestInit)); const response = await this.fetch(new Request(requestUrl, requestInit));
const responseType = response.headers.get("content-type"); const responseType = response.headers.get("content-type");
@ -1897,6 +1874,51 @@ export class ApiService implements ApiServiceAbstraction {
} }
} }
private async buildHeadersAndBody(
authed: boolean,
hasResponse: boolean,
body: any,
alterHeaders: (headers: Headers) => void,
): Promise<[Headers, any]> {
let requestBody: any = null;
const headers = new Headers({
"Device-Type": this.deviceType,
});
if (flagEnabled("prereleaseBuild")) {
headers.set("Is-Prerelease", "1");
}
if (this.customUserAgent != null) {
headers.set("User-Agent", this.customUserAgent);
}
if (hasResponse) {
headers.set("Accept", "application/json");
}
if (alterHeaders != null) {
alterHeaders(headers);
}
if (authed) {
const authHeader = await this.getActiveBearerToken();
headers.set("Authorization", "Bearer " + authHeader);
}
if (body != null) {
if (typeof body === "string") {
requestBody = body;
headers.set("Content-Type", "application/x-www-form-urlencoded; charset=utf-8");
} else if (typeof body === "object") {
if (body instanceof FormData) {
requestBody = body;
} else {
headers.set("Content-Type", "application/json; charset=utf-8");
requestBody = JSON.stringify(body);
}
}
}
return [headers, requestBody];
}
private async handleError( private async handleError(
response: Response, response: Response,
tokenError: boolean, tokenError: boolean,

View File

@ -22,6 +22,7 @@ export type ObjectKey<State, Secret = State, Disclosed = Record<string, never>>
classifier: Classifier<State, Disclosed, Secret>; classifier: Classifier<State, Disclosed, Secret>;
format: "plain" | "classified"; format: "plain" | "classified";
options: UserKeyDefinitionOptions<State>; options: UserKeyDefinitionOptions<State>;
initial?: State;
}; };
export function isObjectKey(key: any): key is ObjectKey<unknown> { export function isObjectKey(key: any): key is ObjectKey<unknown> {

View File

@ -254,17 +254,18 @@ export class UserStateSubject<
withConstraints, withConstraints,
map(([loadedState, constraints]) => { map(([loadedState, constraints]) => {
// bypass nulls // bypass nulls
if (!loadedState) { if (!loadedState && !this.objectKey?.initial) {
return { return {
constraints: {} as Constraints<State>, constraints: {} as Constraints<State>,
state: null, state: null,
} satisfies Constrained<State>; } satisfies Constrained<State>;
} }
const unconstrained = loadedState ?? structuredClone(this.objectKey.initial);
const calibration = isDynamic(constraints) const calibration = isDynamic(constraints)
? constraints.calibrate(loadedState) ? constraints.calibrate(unconstrained)
: constraints; : constraints;
const adjusted = calibration.adjust(loadedState); const adjusted = calibration.adjust(unconstrained);
return { return {
constraints: calibration.constraints, constraints: calibration.constraints,

View File

@ -119,7 +119,7 @@ export abstract class CipherService implements UserKeyRotationDataProvider<Ciphe
* Used for Unassigned ciphers or when the user only has admin access to the cipher (not assigned normally). * Used for Unassigned ciphers or when the user only has admin access to the cipher (not assigned normally).
* @param cipher * @param cipher
*/ */
saveCollectionsWithServerAdmin: (cipher: Cipher) => Promise<void>; saveCollectionsWithServerAdmin: (cipher: Cipher) => Promise<Cipher>;
/** /**
* Bulk update collections for many ciphers with the server * Bulk update collections for many ciphers with the server
* @param orgId * @param orgId

View File

@ -880,9 +880,11 @@ export class CipherService implements CipherServiceAbstraction {
return new Cipher(updated[cipher.id as CipherId], cipher.localData); return new Cipher(updated[cipher.id as CipherId], cipher.localData);
} }
async saveCollectionsWithServerAdmin(cipher: Cipher): Promise<void> { async saveCollectionsWithServerAdmin(cipher: Cipher): Promise<Cipher> {
const request = new CipherCollectionsRequest(cipher.collectionIds); const request = new CipherCollectionsRequest(cipher.collectionIds);
await this.apiService.putCipherCollectionsAdmin(cipher.id, request); const response = await this.apiService.putCipherCollectionsAdmin(cipher.id, request);
const data = new CipherData(response);
return new Cipher(data);
} }
/** /**

View File

@ -0,0 +1,27 @@
import { Directive, HostBinding, HostListener, Input } from "@angular/core";
import { DisclosureComponent } from "./disclosure.component";
@Directive({
selector: "[bitDisclosureTriggerFor]",
exportAs: "disclosureTriggerFor",
standalone: true,
})
export class DisclosureTriggerForDirective {
/**
* Accepts template reference for a bit-disclosure component instance
*/
@Input("bitDisclosureTriggerFor") disclosure: DisclosureComponent;
@HostBinding("attr.aria-expanded") get ariaExpanded() {
return this.disclosure.open;
}
@HostBinding("attr.aria-controls") get ariaControls() {
return this.disclosure.id;
}
@HostListener("click") click() {
this.disclosure.open = !this.disclosure.open;
}
}

Some files were not shown because too many files have changed in this diff Show More