mirror of
https://github.com/bitwarden/browser
synced 2025-12-15 15:53:27 +00:00
Merge branch 'main' into task/DEVOPS-1683
This commit is contained in:
2
.github/workflows/auto-branch-updater.yml
vendored
2
.github/workflows/auto-branch-updater.yml
vendored
@@ -4,7 +4,7 @@ name: Auto Update Branch
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- 'master'
|
- 'main'
|
||||||
- 'rc'
|
- 'rc'
|
||||||
paths:
|
paths:
|
||||||
- 'apps/web/**'
|
- 'apps/web/**'
|
||||||
|
|||||||
8
.github/workflows/build-browser.yml
vendored
8
.github/workflows/build-browser.yml
vendored
@@ -14,7 +14,7 @@ on:
|
|||||||
- '!*.txt'
|
- '!*.txt'
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- 'master'
|
- 'main'
|
||||||
- 'rc'
|
- 'rc'
|
||||||
- 'hotfix-rc-browser'
|
- 'hotfix-rc-browser'
|
||||||
paths:
|
paths:
|
||||||
@@ -351,7 +351,7 @@ jobs:
|
|||||||
|
|
||||||
crowdin-push:
|
crowdin-push:
|
||||||
name: Crowdin Push
|
name: Crowdin Push
|
||||||
if: github.ref == 'refs/heads/master'
|
if: github.ref == 'refs/heads/main'
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
needs:
|
needs:
|
||||||
- build
|
- build
|
||||||
@@ -380,7 +380,7 @@ jobs:
|
|||||||
CROWDIN_PROJECT_ID: "268134"
|
CROWDIN_PROJECT_ID: "268134"
|
||||||
with:
|
with:
|
||||||
config: apps/browser/crowdin.yml
|
config: apps/browser/crowdin.yml
|
||||||
crowdin_branch_name: master
|
crowdin_branch_name: main
|
||||||
upload_sources: true
|
upload_sources: true
|
||||||
upload_translations: false
|
upload_translations: false
|
||||||
|
|
||||||
@@ -397,7 +397,7 @@ jobs:
|
|||||||
- crowdin-push
|
- crowdin-push
|
||||||
steps:
|
steps:
|
||||||
- name: Check if any job failed
|
- name: Check if any job failed
|
||||||
if: ${{ (github.ref == 'refs/heads/master') || (github.ref == 'refs/heads/rc') }}
|
if: ${{ (github.ref == 'refs/heads/main') || (github.ref == 'refs/heads/rc') }}
|
||||||
env:
|
env:
|
||||||
CLOC_STATUS: ${{ needs.cloc.result }}
|
CLOC_STATUS: ${{ needs.cloc.result }}
|
||||||
SETUP_STATUS: ${{ needs.setup.result }}
|
SETUP_STATUS: ${{ needs.setup.result }}
|
||||||
|
|||||||
4
.github/workflows/build-cli.yml
vendored
4
.github/workflows/build-cli.yml
vendored
@@ -15,7 +15,7 @@ on:
|
|||||||
- '.github/workflows/build-cli.yml'
|
- '.github/workflows/build-cli.yml'
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- 'master'
|
- 'main'
|
||||||
- 'rc'
|
- 'rc'
|
||||||
- 'hotfix-rc-cli'
|
- 'hotfix-rc-cli'
|
||||||
paths:
|
paths:
|
||||||
@@ -379,7 +379,7 @@ jobs:
|
|||||||
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/master') || (github.ref == 'refs/heads/rc') }}
|
if: ${{ (github.ref == 'refs/heads/main') || (github.ref == 'refs/heads/rc') }}
|
||||||
env:
|
env:
|
||||||
CLOC_STATUS: ${{ needs.cloc.result }}
|
CLOC_STATUS: ${{ needs.cloc.result }}
|
||||||
SETUP_STATUS: ${{ needs.setup.result }}
|
SETUP_STATUS: ${{ needs.setup.result }}
|
||||||
|
|||||||
10
.github/workflows/build-desktop.yml
vendored
10
.github/workflows/build-desktop.yml
vendored
@@ -15,7 +15,7 @@ on:
|
|||||||
- '.github/workflows/build-desktop.yml'
|
- '.github/workflows/build-desktop.yml'
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- 'master'
|
- 'main'
|
||||||
- 'rc'
|
- 'rc'
|
||||||
- 'hotfix-rc-desktop'
|
- 'hotfix-rc-desktop'
|
||||||
paths:
|
paths:
|
||||||
@@ -973,7 +973,7 @@ jobs:
|
|||||||
APPLE_ID_USERNAME: ${{ secrets.APPLE_ID_USERNAME }}
|
APPLE_ID_USERNAME: ${{ secrets.APPLE_ID_USERNAME }}
|
||||||
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
|
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
|
||||||
if: |
|
if: |
|
||||||
(github.ref == 'refs/heads/master'
|
(github.ref == 'refs/heads/main'
|
||||||
&& needs.setup.outputs.rc_branch_exists == 0
|
&& needs.setup.outputs.rc_branch_exists == 0
|
||||||
&& needs.setup.outputs.hotfix_branch_exists == 0)
|
&& needs.setup.outputs.hotfix_branch_exists == 0)
|
||||||
|| (github.ref == 'refs/heads/rc' && needs.setup.outputs.hotfix_branch_exists == 0)
|
|| (github.ref == 'refs/heads/rc' && needs.setup.outputs.hotfix_branch_exists == 0)
|
||||||
@@ -1154,7 +1154,7 @@ jobs:
|
|||||||
|
|
||||||
crowdin-push:
|
crowdin-push:
|
||||||
name: Crowdin Push
|
name: Crowdin Push
|
||||||
if: github.ref == 'refs/heads/master'
|
if: github.ref == 'refs/heads/main'
|
||||||
needs:
|
needs:
|
||||||
- linux
|
- linux
|
||||||
- windows
|
- windows
|
||||||
@@ -1185,7 +1185,7 @@ jobs:
|
|||||||
CROWDIN_PROJECT_ID: "299360"
|
CROWDIN_PROJECT_ID: "299360"
|
||||||
with:
|
with:
|
||||||
config: apps/desktop/crowdin.yml
|
config: apps/desktop/crowdin.yml
|
||||||
crowdin_branch_name: master
|
crowdin_branch_name: main
|
||||||
upload_sources: true
|
upload_sources: true
|
||||||
upload_translations: false
|
upload_translations: false
|
||||||
|
|
||||||
@@ -1207,7 +1207,7 @@ jobs:
|
|||||||
- crowdin-push
|
- crowdin-push
|
||||||
steps:
|
steps:
|
||||||
- name: Check if any job failed
|
- name: Check if any job failed
|
||||||
if: ${{ (github.ref == 'refs/heads/master') || (github.ref == 'refs/heads/rc') }}
|
if: ${{ (github.ref == 'refs/heads/main') || (github.ref == 'refs/heads/rc') }}
|
||||||
env:
|
env:
|
||||||
CLOC_STATUS: ${{ needs.cloc.result }}
|
CLOC_STATUS: ${{ needs.cloc.result }}
|
||||||
ELECTRON_VERIFY_STATUS: ${{ needs.electron-verify.result }}
|
ELECTRON_VERIFY_STATUS: ${{ needs.electron-verify.result }}
|
||||||
|
|||||||
12
.github/workflows/build-web.yml
vendored
12
.github/workflows/build-web.yml
vendored
@@ -15,7 +15,7 @@ on:
|
|||||||
- '.github/workflows/build-web.yml'
|
- '.github/workflows/build-web.yml'
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- 'master'
|
- 'main'
|
||||||
- 'rc'
|
- 'rc'
|
||||||
- 'hotfix-rc-web'
|
- 'hotfix-rc-web'
|
||||||
paths:
|
paths:
|
||||||
@@ -170,7 +170,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Check Branch to Publish
|
- name: Check Branch to Publish
|
||||||
env:
|
env:
|
||||||
PUBLISH_BRANCHES: "master,rc,hotfix-rc-web"
|
PUBLISH_BRANCHES: "main,rc,hotfix-rc-web"
|
||||||
id: publish-branch-check
|
id: publish-branch-check
|
||||||
run: |
|
run: |
|
||||||
IFS="," read -a publish_branches <<< $PUBLISH_BRANCHES
|
IFS="," read -a publish_branches <<< $PUBLISH_BRANCHES
|
||||||
@@ -218,7 +218,7 @@ jobs:
|
|||||||
IMAGE_TAG=$(echo "${GITHUB_REF:11}" | sed "s#/#-#g")
|
IMAGE_TAG=$(echo "${GITHUB_REF:11}" | sed "s#/#-#g")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$IMAGE_TAG" == "master" ]]; then
|
if [[ "$IMAGE_TAG" == "main" ]]; then
|
||||||
IMAGE_TAG=dev
|
IMAGE_TAG=dev
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -259,7 +259,7 @@ jobs:
|
|||||||
|
|
||||||
crowdin-push:
|
crowdin-push:
|
||||||
name: Crowdin Push
|
name: Crowdin Push
|
||||||
if: github.ref == 'refs/heads/master'
|
if: github.ref == 'refs/heads/main'
|
||||||
needs: build-artifacts
|
needs: build-artifacts
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
@@ -286,7 +286,7 @@ jobs:
|
|||||||
CROWDIN_PROJECT_ID: "308189"
|
CROWDIN_PROJECT_ID: "308189"
|
||||||
with:
|
with:
|
||||||
config: apps/web/crowdin.yml
|
config: apps/web/crowdin.yml
|
||||||
crowdin_branch_name: master
|
crowdin_branch_name: main
|
||||||
upload_sources: true
|
upload_sources: true
|
||||||
upload_translations: false
|
upload_translations: false
|
||||||
|
|
||||||
@@ -303,7 +303,7 @@ jobs:
|
|||||||
- crowdin-push
|
- crowdin-push
|
||||||
steps:
|
steps:
|
||||||
- name: Check if any job failed
|
- name: Check if any job failed
|
||||||
if: ${{ (github.ref == 'refs/heads/master') || (github.ref == 'refs/heads/rc') }}
|
if: ${{ (github.ref == 'refs/heads/main') || (github.ref == 'refs/heads/rc') }}
|
||||||
env:
|
env:
|
||||||
CLOC_STATUS: ${{ needs.cloc.result }}
|
CLOC_STATUS: ${{ needs.cloc.result }}
|
||||||
SETUP_STATUS: ${{ needs.setup.result }}
|
SETUP_STATUS: ${{ needs.setup.result }}
|
||||||
|
|||||||
2
.github/workflows/crowdin-pull.yml
vendored
2
.github/workflows/crowdin-pull.yml
vendored
@@ -45,7 +45,7 @@ jobs:
|
|||||||
CROWDIN_PROJECT_ID: ${{ matrix.crowdin_project_id }}
|
CROWDIN_PROJECT_ID: ${{ matrix.crowdin_project_id }}
|
||||||
with:
|
with:
|
||||||
config: crowdin.yml
|
config: crowdin.yml
|
||||||
crowdin_branch_name: master
|
crowdin_branch_name: main
|
||||||
upload_sources: false
|
upload_sources: false
|
||||||
upload_translations: false
|
upload_translations: false
|
||||||
download_translations: true
|
download_translations: true
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Runs creation of Pull Requests
|
# Runs creation of Pull Requests
|
||||||
# If the PR destination branch is master, add a needs-qa label unless created by renovate[bot]
|
# If the PR destination branch is main, add a needs-qa label unless created by renovate[bot]
|
||||||
---
|
---
|
||||||
name: Label Issue Pull Request
|
name: Label Issue Pull Request
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ on:
|
|||||||
paths-ignore:
|
paths-ignore:
|
||||||
- .github/workflows/** # We don't need QA on workflow changes
|
- .github/workflows/** # We don't need QA on workflow changes
|
||||||
branches:
|
branches:
|
||||||
- 'master' # We only want to check when PRs target master
|
- 'main' # We only want to check when PRs target main
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
add-needs-qa-label:
|
add-needs-qa-label:
|
||||||
|
|||||||
4
.github/workflows/release-browser.yml
vendored
4
.github/workflows/release-browser.yml
vendored
@@ -114,13 +114,13 @@ jobs:
|
|||||||
dist-firefox-*.zip,
|
dist-firefox-*.zip,
|
||||||
dist-edge-*.zip'
|
dist-edge-*.zip'
|
||||||
|
|
||||||
- name: Dry Run - Download latest master build artifacts
|
- name: Dry Run - Download latest build artifacts
|
||||||
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
|
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
|
||||||
uses: bitwarden/gh-actions/download-artifacts@main
|
uses: bitwarden/gh-actions/download-artifacts@main
|
||||||
with:
|
with:
|
||||||
workflow: build-browser.yml
|
workflow: build-browser.yml
|
||||||
workflow_conclusion: success
|
workflow_conclusion: success
|
||||||
branch: master
|
branch: main
|
||||||
artifacts: 'browser-source-*.zip,
|
artifacts: 'browser-source-*.zip,
|
||||||
dist-chrome-*.zip,
|
dist-chrome-*.zip,
|
||||||
dist-opera-*.zip,
|
dist-opera-*.zip,
|
||||||
|
|||||||
8
.github/workflows/release-cli.yml
vendored
8
.github/workflows/release-cli.yml
vendored
@@ -92,7 +92,7 @@ jobs:
|
|||||||
workflow: build-cli.yml
|
workflow: build-cli.yml
|
||||||
path: apps/cli
|
path: apps/cli
|
||||||
workflow_conclusion: success
|
workflow_conclusion: success
|
||||||
branch: master
|
branch: main
|
||||||
|
|
||||||
- name: Create release
|
- name: Create release
|
||||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||||
@@ -175,7 +175,7 @@ jobs:
|
|||||||
workflow: build-cli.yml
|
workflow: build-cli.yml
|
||||||
path: apps/cli
|
path: apps/cli
|
||||||
workflow_conclusion: success
|
workflow_conclusion: success
|
||||||
branch: master
|
branch: main
|
||||||
artifacts: bw_${{ env._PKG_VERSION }}_amd64.snap
|
artifacts: bw_${{ env._PKG_VERSION }}_amd64.snap
|
||||||
|
|
||||||
- name: Publish Snap & logout
|
- name: Publish Snap & logout
|
||||||
@@ -235,7 +235,7 @@ jobs:
|
|||||||
workflow: build-cli.yml
|
workflow: build-cli.yml
|
||||||
path: apps/cli/dist
|
path: apps/cli/dist
|
||||||
workflow_conclusion: success
|
workflow_conclusion: success
|
||||||
branch: master
|
branch: main
|
||||||
artifacts: bitwarden-cli.${{ env._PKG_VERSION }}.nupkg
|
artifacts: bitwarden-cli.${{ env._PKG_VERSION }}.nupkg
|
||||||
|
|
||||||
- name: Push to Chocolatey
|
- name: Push to Chocolatey
|
||||||
@@ -285,7 +285,7 @@ jobs:
|
|||||||
workflow: build-cli.yml
|
workflow: build-cli.yml
|
||||||
path: apps/cli/build
|
path: apps/cli/build
|
||||||
workflow_conclusion: success
|
workflow_conclusion: success
|
||||||
branch: master
|
branch: main
|
||||||
artifacts: bitwarden-cli-${{ env._PKG_VERSION }}-npm-build.zip
|
artifacts: bitwarden-cli-${{ env._PKG_VERSION }}-npm-build.zip
|
||||||
|
|
||||||
- name: Setup NPM
|
- name: Setup NPM
|
||||||
|
|||||||
12
.github/workflows/release-desktop-beta.yml
vendored
12
.github/workflows/release-desktop-beta.yml
vendored
@@ -28,9 +28,9 @@ jobs:
|
|||||||
|
|
||||||
- name: Branch check
|
- name: Branch check
|
||||||
run: |
|
run: |
|
||||||
if [[ "$GITHUB_REF" != "refs/heads/master" ]] && [[ "$GITHUB_REF" != "refs/heads/rc" ]] && [[ "$GITHUB_REF" != "refs/heads/hotfix-rc" ]]; then
|
if [[ "$GITHUB_REF" != "refs/heads/main" ]] && [[ "$GITHUB_REF" != "refs/heads/rc" ]] && [[ "$GITHUB_REF" != "refs/heads/hotfix-rc" ]]; then
|
||||||
echo "==================================="
|
echo "==================================="
|
||||||
echo "[!] Can only release from the 'master', 'rc' or 'hotfix-rc' branches"
|
echo "[!] Can only release from the 'main', 'rc' or 'hotfix-rc' branches"
|
||||||
echo "==================================="
|
echo "==================================="
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -661,13 +661,13 @@ jobs:
|
|||||||
branch: rc
|
branch: rc
|
||||||
path: ${{ github.workspace }}/browser-build-artifacts
|
path: ${{ github.workspace }}/browser-build-artifacts
|
||||||
|
|
||||||
- name: Download artifact from master
|
- name: Download artifacts from main
|
||||||
if: ${{ github.ref != 'refs/heads/rc' && github.ref != 'refs/heads/hotfix-rc' }}
|
if: ${{ github.ref != 'refs/heads/rc' && github.ref != 'refs/heads/hotfix-rc' }}
|
||||||
uses: dawidd6/action-download-artifact@246dbf436b23d7c49e21a7ab8204ca9ecd1fe615 # v2.27.0
|
uses: dawidd6/action-download-artifact@246dbf436b23d7c49e21a7ab8204ca9ecd1fe615 # v2.27.0
|
||||||
with:
|
with:
|
||||||
workflow: build-browser.yml
|
workflow: build-browser.yml
|
||||||
workflow_conclusion: success
|
workflow_conclusion: success
|
||||||
branch: master
|
branch: main
|
||||||
path: ${{ github.workspace }}/browser-build-artifacts
|
path: ${{ github.workspace }}/browser-build-artifacts
|
||||||
|
|
||||||
- name: Unzip Safari artifact
|
- name: Unzip Safari artifact
|
||||||
@@ -859,13 +859,13 @@ jobs:
|
|||||||
branch: rc
|
branch: rc
|
||||||
path: ${{ github.workspace }}/browser-build-artifacts
|
path: ${{ github.workspace }}/browser-build-artifacts
|
||||||
|
|
||||||
- name: Download artifact from master
|
- name: Download artifact from main
|
||||||
if: ${{ github.ref != 'refs/heads/rc' && github.ref != 'refs/heads/hotfix-rc' }}
|
if: ${{ github.ref != 'refs/heads/rc' && github.ref != 'refs/heads/hotfix-rc' }}
|
||||||
uses: dawidd6/action-download-artifact@246dbf436b23d7c49e21a7ab8204ca9ecd1fe615 # v2.27.0
|
uses: dawidd6/action-download-artifact@246dbf436b23d7c49e21a7ab8204ca9ecd1fe615 # v2.27.0
|
||||||
with:
|
with:
|
||||||
workflow: build-browser.yml
|
workflow: build-browser.yml
|
||||||
workflow_conclusion: success
|
workflow_conclusion: success
|
||||||
branch: master
|
branch: main
|
||||||
path: ${{ github.workspace }}/browser-build-artifacts
|
path: ${{ github.workspace }}/browser-build-artifacts
|
||||||
|
|
||||||
- name: Unzip Safari artifact
|
- name: Unzip Safari artifact
|
||||||
|
|||||||
6
.github/workflows/release-desktop.yml
vendored
6
.github/workflows/release-desktop.yml
vendored
@@ -136,7 +136,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
workflow: build-desktop.yml
|
workflow: build-desktop.yml
|
||||||
workflow_conclusion: success
|
workflow_conclusion: success
|
||||||
branch: master
|
branch: main
|
||||||
path: apps/desktop/artifacts
|
path: apps/desktop/artifacts
|
||||||
|
|
||||||
- name: Rename .pkg to .pkg.archive
|
- name: Rename .pkg to .pkg.archive
|
||||||
@@ -291,7 +291,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
workflow: build-desktop.yml
|
workflow: build-desktop.yml
|
||||||
workflow_conclusion: success
|
workflow_conclusion: success
|
||||||
branch: master
|
branch: main
|
||||||
artifacts: bitwarden_${{ env._PKG_VERSION }}_amd64.snap
|
artifacts: bitwarden_${{ env._PKG_VERSION }}_amd64.snap
|
||||||
path: apps/desktop/dist
|
path: apps/desktop/dist
|
||||||
|
|
||||||
@@ -359,7 +359,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
workflow: build-desktop.yml
|
workflow: build-desktop.yml
|
||||||
workflow_conclusion: success
|
workflow_conclusion: success
|
||||||
branch: master
|
branch: main
|
||||||
artifacts: bitwarden.${{ env._PKG_VERSION }}.nupkg
|
artifacts: bitwarden.${{ env._PKG_VERSION }}.nupkg
|
||||||
path: apps/desktop/dist
|
path: apps/desktop/dist
|
||||||
|
|
||||||
|
|||||||
8
.github/workflows/release-web.yml
vendored
8
.github/workflows/release-web.yml
vendored
@@ -159,7 +159,7 @@ jobs:
|
|||||||
workflow: build-web.yml
|
workflow: build-web.yml
|
||||||
path: assets
|
path: assets
|
||||||
workflow_conclusion: success
|
workflow_conclusion: success
|
||||||
branch: master
|
branch: main
|
||||||
artifacts: web-*-cloud-COMMERCIAL.zip
|
artifacts: web-*-cloud-COMMERCIAL.zip
|
||||||
|
|
||||||
- name: Unzip build asset
|
- name: Unzip build asset
|
||||||
@@ -196,12 +196,12 @@ jobs:
|
|||||||
gh pr create --title "Deploy v${_RELEASE_VERSION} to GitHub Pages" \
|
gh pr create --title "Deploy v${_RELEASE_VERSION} to GitHub Pages" \
|
||||||
--draft \
|
--draft \
|
||||||
--body "Deploying v${_RELEASE_VERSION}" \
|
--body "Deploying v${_RELEASE_VERSION}" \
|
||||||
--base master \
|
--base main \
|
||||||
--head "${_BRANCH}"
|
--head "${_BRANCH}"
|
||||||
else
|
else
|
||||||
gh pr create --title "Deploy v${_RELEASE_VERSION} to GitHub Pages" \
|
gh pr create --title "Deploy v${_RELEASE_VERSION} to GitHub Pages" \
|
||||||
--body "Deploying v${_RELEASE_VERSION}" \
|
--body "Deploying v${_RELEASE_VERSION}" \
|
||||||
--base master \
|
--base main \
|
||||||
--head "${_BRANCH}"
|
--head "${_BRANCH}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -243,7 +243,7 @@ jobs:
|
|||||||
workflow: build-web.yml
|
workflow: build-web.yml
|
||||||
path: apps/web/artifacts
|
path: apps/web/artifacts
|
||||||
workflow_conclusion: success
|
workflow_conclusion: success
|
||||||
branch: master
|
branch: main
|
||||||
artifacts: "web-*-selfhosted-COMMERCIAL.zip,
|
artifacts: "web-*-selfhosted-COMMERCIAL.zip,
|
||||||
web-*-selfhosted-open-source.zip"
|
web-*-selfhosted-open-source.zip"
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/stale-bot.yml
vendored
2
.github/workflows/stale-bot.yml
vendored
@@ -27,4 +27,4 @@ jobs:
|
|||||||
|
|
||||||
If you’re still working on this, please respond here after you’ve made the changes we’ve requested and our team will re-open it for further review.
|
If you’re still working on this, please respond here after you’ve made the changes we’ve requested and our team will re-open it for further review.
|
||||||
|
|
||||||
Please make sure to resolve any conflicts with the master branch before requesting another review.
|
Please make sure to resolve any conflicts with the main branch before requesting another review.
|
||||||
|
|||||||
2
.github/workflows/version-bump.yml
vendored
2
.github/workflows/version-bump.yml
vendored
@@ -286,7 +286,7 @@ jobs:
|
|||||||
TITLE: "Bump ${{ steps.create-branch.outputs.client }} version to ${{ inputs.version_number }}"
|
TITLE: "Bump ${{ steps.create-branch.outputs.client }} version to ${{ inputs.version_number }}"
|
||||||
run: |
|
run: |
|
||||||
PR_URL=$(gh pr create --title "$TITLE" \
|
PR_URL=$(gh pr create --title "$TITLE" \
|
||||||
--base "master" \
|
--base "main" \
|
||||||
--head "$PR_BRANCH" \
|
--head "$PR_BRANCH" \
|
||||||
--label "version update" \
|
--label "version update" \
|
||||||
--label "automated pr" \
|
--label "automated pr" \
|
||||||
|
|||||||
@@ -268,6 +268,9 @@
|
|||||||
"length": {
|
"length": {
|
||||||
"message": "Length"
|
"message": "Length"
|
||||||
},
|
},
|
||||||
|
"passwordMinLength": {
|
||||||
|
"message": "Minimum password length"
|
||||||
|
},
|
||||||
"uppercase": {
|
"uppercase": {
|
||||||
"message": "Uppercase (A-Z)"
|
"message": "Uppercase (A-Z)"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
<div class="tw-grid tw-gap-2">
|
<div class="tw-grid tw-gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="tw-flex tw-w-full tw-items-center tw-gap-3 tw-rounded-md tw-bg-background tw-p-3 hover:tw-bg-background-alt disabled:tw-cursor-not-allowed disabled:tw-border-text-muted/60 disabled:!tw-text-muted/60"
|
class="account-switcher-row tw-flex tw-w-full tw-items-center tw-gap-3 tw-rounded-md tw-p-3 disabled:tw-cursor-not-allowed disabled:tw-border-text-muted/60 disabled:!tw-text-muted/60"
|
||||||
(click)="lock()"
|
(click)="lock()"
|
||||||
[disabled]="currentAccount.status === lockedStatus || !activeUserCanLock"
|
[disabled]="currentAccount.status === lockedStatus || !activeUserCanLock"
|
||||||
[title]="!activeUserCanLock ? ('unlockMethodNeeded' | i18n) : ''"
|
[title]="!activeUserCanLock ? ('unlockMethodNeeded' | i18n) : ''"
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="tw-flex tw-w-full tw-items-center tw-gap-3 tw-rounded-md tw-bg-background tw-p-3 hover:tw-bg-background-alt"
|
class="account-switcher-row tw-flex tw-w-full tw-items-center tw-gap-3 tw-rounded-md tw-p-3"
|
||||||
(click)="logOut()"
|
(click)="logOut()"
|
||||||
>
|
>
|
||||||
<i class="bwi bwi-sign-out tw-text-2xl" aria-hidden="true"></i>
|
<i class="bwi bwi-sign-out tw-text-2xl" aria-hidden="true"></i>
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="tw-mt-2 tw-flex tw-w-full tw-items-center tw-gap-3 tw-rounded-md tw-bg-background tw-p-3 hover:tw-bg-background-alt"
|
class="account-switcher-row tw-mt-2 tw-flex tw-w-full tw-items-center tw-gap-3 tw-rounded-md tw-p-3"
|
||||||
(click)="lockAll()"
|
(click)="lockAll()"
|
||||||
>
|
>
|
||||||
<i class="bwi bwi-lock tw-text-2xl" aria-hidden="true"></i>
|
<i class="bwi bwi-lock tw-text-2xl" aria-hidden="true"></i>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<button
|
<button
|
||||||
*ngIf="account.id !== specialAccountAddId"
|
*ngIf="account.id !== specialAccountAddId"
|
||||||
type="button"
|
type="button"
|
||||||
class="tw-flex tw-w-full tw-items-center tw-gap-3 tw-rounded-md tw-border-none tw-bg-background tw-p-3 hover:tw-bg-background-alt"
|
class="account-switcher-row tw-flex tw-w-full tw-items-center tw-gap-3 tw-rounded-md tw-border-none tw-p-3"
|
||||||
(click)="selectAccount(account.id)"
|
(click)="selectAccount(account.id)"
|
||||||
>
|
>
|
||||||
<div class="tw-flex-shrink-0">
|
<div class="tw-flex-shrink-0">
|
||||||
@@ -18,32 +18,35 @@
|
|||||||
<span class="tw-sr-only" *ngIf="status.text !== 'active'">
|
<span class="tw-sr-only" *ngIf="status.text !== 'active'">
|
||||||
{{ "switchToAccount" | i18n }}
|
{{ "switchToAccount" | i18n }}
|
||||||
</span>
|
</span>
|
||||||
<div class="tw-max-w-64 tw-truncate tw-text-main">
|
<div class="tw-max-w-64 tw-truncate">
|
||||||
{{ account.email }}
|
{{ account.email }}
|
||||||
</div>
|
</div>
|
||||||
<div class="tw-max-w-64 tw-truncate tw-text-sm tw-text-muted">
|
<div class="account-switcher-row-details tw-max-w-64 tw-truncate tw-text-sm">
|
||||||
<span class="tw-sr-only">{{ "hostedAt" | i18n }}</span>
|
<span class="tw-sr-only">{{ "hostedAt" | i18n }}</span>
|
||||||
{{ account.server }}
|
{{ account.server }}
|
||||||
</div>
|
</div>
|
||||||
<div class="tw-text-sm tw-italic tw-text-muted" [attr.aria-hidden]="status.text === 'active'">
|
<div
|
||||||
|
class="account-switcher-row-details tw-text-sm tw-italic"
|
||||||
|
[attr.aria-hidden]="status.text === 'active'"
|
||||||
|
>
|
||||||
<span class="tw-sr-only">(</span>
|
<span class="tw-sr-only">(</span>
|
||||||
{{ status.text }}
|
{{ status.text }}
|
||||||
<span class="tw-sr-only">)</span>
|
<span class="tw-sr-only">)</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tw-ml-auto tw-flex-shrink-0">
|
<div class="tw-ml-auto tw-flex-shrink-0">
|
||||||
<i class="bwi tw-text-2xl tw-text-main" [ngClass]="status.icon" aria-hidden="true"></i>
|
<i class="bwi tw-text-2xl" [ngClass]="status.icon" aria-hidden="true"></i>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
*ngIf="account.id === specialAccountAddId"
|
*ngIf="account.id === specialAccountAddId"
|
||||||
type="button"
|
type="button"
|
||||||
class="tw-flex tw-w-full tw-items-center tw-gap-3 tw-rounded-md tw-border-none tw-bg-background tw-p-3 hover:tw-bg-background-alt"
|
class="account-switcher-row tw-flex tw-w-full tw-items-center tw-gap-3 tw-rounded-md tw-border-none tw-p-3"
|
||||||
(click)="selectAccount(account.id)"
|
(click)="selectAccount(account.id)"
|
||||||
>
|
>
|
||||||
<i class="bwi bwi-plus tw-text-2xl tw-text-main" aria-hidden="true"></i>
|
<i class="bwi bwi-plus tw-text-2xl" aria-hidden="true"></i>
|
||||||
<div class="tw-text-main">
|
<div>
|
||||||
{{ account.name }}
|
{{ account.name }}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -156,7 +156,9 @@ import { BrowserSendService } from "../services/browser-send.service";
|
|||||||
import { BrowserSettingsService } from "../services/browser-settings.service";
|
import { BrowserSettingsService } from "../services/browser-settings.service";
|
||||||
import VaultTimeoutService from "../services/vault-timeout/vault-timeout.service";
|
import VaultTimeoutService from "../services/vault-timeout/vault-timeout.service";
|
||||||
import { BrowserFido2UserInterfaceService } from "../vault/fido2/browser-fido2-user-interface.service";
|
import { BrowserFido2UserInterfaceService } from "../vault/fido2/browser-fido2-user-interface.service";
|
||||||
|
import { Fido2Service as Fido2ServiceAbstraction } from "../vault/services/abstractions/fido2.service";
|
||||||
import { BrowserFolderService } from "../vault/services/browser-folder.service";
|
import { BrowserFolderService } from "../vault/services/browser-folder.service";
|
||||||
|
import Fido2Service from "../vault/services/fido2.service";
|
||||||
import { VaultFilterService } from "../vault/services/vault-filter.service";
|
import { VaultFilterService } from "../vault/services/vault-filter.service";
|
||||||
|
|
||||||
import CommandsBackground from "./commands.background";
|
import CommandsBackground from "./commands.background";
|
||||||
@@ -232,6 +234,7 @@ export default class MainBackground {
|
|||||||
authRequestCryptoService: AuthRequestCryptoServiceAbstraction;
|
authRequestCryptoService: AuthRequestCryptoServiceAbstraction;
|
||||||
accountService: AccountServiceAbstraction;
|
accountService: AccountServiceAbstraction;
|
||||||
globalStateProvider: GlobalStateProvider;
|
globalStateProvider: GlobalStateProvider;
|
||||||
|
fido2Service: Fido2ServiceAbstraction;
|
||||||
|
|
||||||
// Passed to the popup for Safari to workaround issues with theming, downloading, etc.
|
// Passed to the popup for Safari to workaround issues with theming, downloading, etc.
|
||||||
backgroundWindow = window;
|
backgroundWindow = window;
|
||||||
@@ -597,6 +600,7 @@ export default class MainBackground {
|
|||||||
this.messagingService,
|
this.messagingService,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.fido2Service = new Fido2Service();
|
||||||
this.fido2UserInterfaceService = new BrowserFido2UserInterfaceService(this.authService);
|
this.fido2UserInterfaceService = new BrowserFido2UserInterfaceService(this.authService);
|
||||||
this.fido2AuthenticatorService = new Fido2AuthenticatorService(
|
this.fido2AuthenticatorService = new Fido2AuthenticatorService(
|
||||||
this.cipherService,
|
this.cipherService,
|
||||||
@@ -645,6 +649,7 @@ export default class MainBackground {
|
|||||||
this.messagingService,
|
this.messagingService,
|
||||||
this.logService,
|
this.logService,
|
||||||
this.configService,
|
this.configService,
|
||||||
|
this.fido2Service,
|
||||||
);
|
);
|
||||||
this.nativeMessagingBackground = new NativeMessagingBackground(
|
this.nativeMessagingBackground = new NativeMessagingBackground(
|
||||||
this.cryptoService,
|
this.cryptoService,
|
||||||
@@ -778,6 +783,8 @@ export default class MainBackground {
|
|||||||
await this.idleBackground.init();
|
await this.idleBackground.init();
|
||||||
await this.webRequestBackground.init();
|
await this.webRequestBackground.init();
|
||||||
|
|
||||||
|
await this.fido2Service.init();
|
||||||
|
|
||||||
if (this.platformUtilsService.isFirefox() && !this.isPrivateMode) {
|
if (this.platformUtilsService.isFirefox() && !this.isPrivateMode) {
|
||||||
// Set Private Mode windows to the default icon - they do not share state with the background page
|
// Set Private Mode windows to the default icon - they do not share state with the background page
|
||||||
const privateWindows = await BrowserApi.getPrivateModeWindows();
|
const privateWindows = await BrowserApi.getPrivateModeWindows();
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { BrowserStateService } from "../platform/services/abstractions/browser-s
|
|||||||
import { BrowserEnvironmentService } from "../platform/services/browser-environment.service";
|
import { BrowserEnvironmentService } from "../platform/services/browser-environment.service";
|
||||||
import BrowserPlatformUtilsService from "../platform/services/browser-platform-utils.service";
|
import BrowserPlatformUtilsService from "../platform/services/browser-platform-utils.service";
|
||||||
import { AbortManager } from "../vault/background/abort-manager";
|
import { AbortManager } from "../vault/background/abort-manager";
|
||||||
|
import { Fido2Service } from "../vault/services/abstractions/fido2.service";
|
||||||
|
|
||||||
import MainBackground from "./main.background";
|
import MainBackground from "./main.background";
|
||||||
|
|
||||||
@@ -42,6 +43,7 @@ export default class RuntimeBackground {
|
|||||||
private messagingService: MessagingService,
|
private messagingService: MessagingService,
|
||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
private configService: ConfigServiceAbstraction,
|
private configService: ConfigServiceAbstraction,
|
||||||
|
private fido2Service: Fido2Service,
|
||||||
) {
|
) {
|
||||||
// onInstalled listener must be wired up before anything else, so we do it in the ctor
|
// onInstalled listener must be wired up before anything else, so we do it in the ctor
|
||||||
chrome.runtime.onInstalled.addListener((details: any) => {
|
chrome.runtime.onInstalled.addListener((details: any) => {
|
||||||
@@ -257,6 +259,9 @@ export default class RuntimeBackground {
|
|||||||
case "getClickedElementResponse":
|
case "getClickedElementResponse":
|
||||||
this.platformUtilsService.copyToClipboard(msg.identifier, { window: window });
|
this.platformUtilsService.copyToClipboard(msg.identifier, { window: window });
|
||||||
break;
|
break;
|
||||||
|
case "triggerFido2ContentScriptInjection":
|
||||||
|
await this.fido2Service.injectFido2ContentScripts(sender);
|
||||||
|
break;
|
||||||
case "fido2AbortRequest":
|
case "fido2AbortRequest":
|
||||||
this.abortManager.abort(msg.abortedRequestId);
|
this.abortManager.abort(msg.abortedRequestId);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -17,7 +17,10 @@
|
|||||||
"content_scripts": [
|
"content_scripts": [
|
||||||
{
|
{
|
||||||
"all_frames": true,
|
"all_frames": true,
|
||||||
"js": ["content/trigger-autofill-script-injection.js", "content/fido2/content-script.js"],
|
"js": [
|
||||||
|
"content/trigger-autofill-script-injection.js",
|
||||||
|
"content/fido2/trigger-fido2-content-script-injection.js"
|
||||||
|
],
|
||||||
"matches": ["http://*/*", "https://*/*", "file:///*"],
|
"matches": ["http://*/*", "https://*/*", "file:///*"],
|
||||||
"run_at": "document_start"
|
"run_at": "document_start"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -341,7 +341,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.img-right {
|
.img-right,
|
||||||
|
.txt-right {
|
||||||
float: right;
|
float: right;
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
@@ -767,3 +768,24 @@ form {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.account-switcher-row {
|
||||||
|
@include themify($themes) {
|
||||||
|
color: themed("textColor");
|
||||||
|
background-color: themed("boxBackgroundColor");
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus,
|
||||||
|
&.active {
|
||||||
|
@include themify($themes) {
|
||||||
|
background-color: themed("listItemBackgroundHoverColor");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-details {
|
||||||
|
@include themify($themes) {
|
||||||
|
color: themed("mutedColor");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -176,7 +176,7 @@
|
|||||||
<input
|
<input
|
||||||
id="length"
|
id="length"
|
||||||
type="number"
|
type="number"
|
||||||
min="5"
|
[min]="passwordOptions.minLength"
|
||||||
max="128"
|
max="128"
|
||||||
[(ngModel)]="passwordOptions.length"
|
[(ngModel)]="passwordOptions.length"
|
||||||
(change)="savePasswordOptions()"
|
(change)="savePasswordOptions()"
|
||||||
@@ -184,7 +184,7 @@
|
|||||||
<input
|
<input
|
||||||
id="lengthRange"
|
id="lengthRange"
|
||||||
type="range"
|
type="range"
|
||||||
min="5"
|
[min]="passwordOptions.minLength"
|
||||||
max="128"
|
max="128"
|
||||||
step="1"
|
step="1"
|
||||||
[(ngModel)]="passwordOptions.length"
|
[(ngModel)]="passwordOptions.length"
|
||||||
@@ -194,6 +194,18 @@
|
|||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="box-content-row" appBoxRow>
|
||||||
|
<span>{{ "passwordMinLength" | i18n }}</span>
|
||||||
|
<span
|
||||||
|
class="sr-only"
|
||||||
|
attr.aria-label="{{ 'passwordMinLength' | i18n }}"
|
||||||
|
role="status"
|
||||||
|
aria-live="polite"
|
||||||
|
>
|
||||||
|
{{ passwordOptionsMinLengthForReader$ | async }}
|
||||||
|
</span>
|
||||||
|
<span class="txt-right">{{ passwordOptions.minLength }}</span>
|
||||||
|
</div>
|
||||||
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
||||||
<label for="uppercase">A-Z</label>
|
<label for="uppercase">A-Z</label>
|
||||||
<input
|
<input
|
||||||
@@ -221,10 +233,10 @@
|
|||||||
<input
|
<input
|
||||||
id="numbers"
|
id="numbers"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
(change)="savePasswordOptions()"
|
|
||||||
attr.aria-label="{{ 'numbers' | i18n }}"
|
attr.aria-label="{{ 'numbers' | i18n }}"
|
||||||
[disabled]="enforcedPasswordPolicyOptions.useNumbers"
|
[disabled]="enforcedPasswordPolicyOptions.useNumbers"
|
||||||
[(ngModel)]="passwordOptions.number"
|
[ngModel]="passwordOptions.number"
|
||||||
|
(ngModelChange)="setPasswordOptionsNumber($event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
||||||
@@ -232,10 +244,10 @@
|
|||||||
<input
|
<input
|
||||||
id="special"
|
id="special"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
(change)="savePasswordOptions()"
|
|
||||||
attr.aria-label="{{ 'specialCharacters' | i18n }}"
|
attr.aria-label="{{ 'specialCharacters' | i18n }}"
|
||||||
[disabled]="enforcedPasswordPolicyOptions.useSpecial"
|
[disabled]="enforcedPasswordPolicyOptions.useSpecial"
|
||||||
[(ngModel)]="passwordOptions.special"
|
[ngModel]="passwordOptions.special"
|
||||||
|
(ngModelChange)="setPasswordOptionsSpecial($event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -249,8 +261,8 @@
|
|||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
max="9"
|
max="9"
|
||||||
(change)="savePasswordOptions()"
|
|
||||||
[(ngModel)]="passwordOptions.minNumber"
|
[(ngModel)]="passwordOptions.minNumber"
|
||||||
|
(input)="onPasswordOptionsMinNumberInput($event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="box-content-row box-content-row-input" appBoxRow>
|
<div class="box-content-row box-content-row-input" appBoxRow>
|
||||||
@@ -260,8 +272,8 @@
|
|||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
max="9"
|
max="9"
|
||||||
(change)="savePasswordOptions()"
|
|
||||||
[(ngModel)]="passwordOptions.minSpecial"
|
[(ngModel)]="passwordOptions.minSpecial"
|
||||||
|
(input)="onPasswordOptionsMinSpecialInput($event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
||||||
|
|||||||
@@ -61,12 +61,28 @@ async function isLocationBitwardenVault(activeUserSettings: Record<string, any>)
|
|||||||
return window.location.origin === activeUserSettings.serverConfig.environment.vault;
|
return window.location.origin === activeUserSettings.serverConfig.environment.vault;
|
||||||
}
|
}
|
||||||
|
|
||||||
function initializeFido2ContentScript() {
|
const messenger = Messenger.forDOMCommunication(window);
|
||||||
const s = document.createElement("script");
|
|
||||||
s.src = chrome.runtime.getURL("content/fido2/page-script.js");
|
|
||||||
(document.head || document.documentElement).appendChild(s);
|
|
||||||
|
|
||||||
const messenger = Messenger.forDOMCommunication(window);
|
function injectPageScript() {
|
||||||
|
// Locate an existing page-script on the page
|
||||||
|
const existingPageScript = document.getElementById("bw-fido2-page-script");
|
||||||
|
|
||||||
|
// Inject the page-script if it doesn't exist
|
||||||
|
if (!existingPageScript) {
|
||||||
|
const s = document.createElement("script");
|
||||||
|
s.src = chrome.runtime.getURL("content/fido2/page-script.js");
|
||||||
|
s.id = "bw-fido2-page-script";
|
||||||
|
(document.head || document.documentElement).appendChild(s);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the page-script already exists, send a reconnect message to the page-script
|
||||||
|
messenger.sendReconnectCommand();
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeFido2ContentScript() {
|
||||||
|
injectPageScript();
|
||||||
|
|
||||||
messenger.handler = async (message, abortController) => {
|
messenger.handler = async (message, abortController) => {
|
||||||
const requestId = Date.now().toString();
|
const requestId = Date.now().toString();
|
||||||
@@ -78,7 +94,7 @@ function initializeFido2ContentScript() {
|
|||||||
abortController.signal.addEventListener("abort", abortHandler);
|
abortController.signal.addEventListener("abort", abortHandler);
|
||||||
|
|
||||||
if (message.type === MessageType.CredentialCreationRequest) {
|
if (message.type === MessageType.CredentialCreationRequest) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise<Message | undefined>((resolve, reject) => {
|
||||||
const data: CreateCredentialParams = {
|
const data: CreateCredentialParams = {
|
||||||
...message.data,
|
...message.data,
|
||||||
origin: window.location.origin,
|
origin: window.location.origin,
|
||||||
@@ -92,7 +108,7 @@ function initializeFido2ContentScript() {
|
|||||||
requestId: requestId,
|
requestId: requestId,
|
||||||
},
|
},
|
||||||
(response) => {
|
(response) => {
|
||||||
if (response.error !== undefined) {
|
if (response && response.error !== undefined) {
|
||||||
return reject(response.error);
|
return reject(response.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,7 +122,7 @@ function initializeFido2ContentScript() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (message.type === MessageType.CredentialGetRequest) {
|
if (message.type === MessageType.CredentialGetRequest) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise<Message | undefined>((resolve, reject) => {
|
||||||
const data: AssertCredentialParams = {
|
const data: AssertCredentialParams = {
|
||||||
...message.data,
|
...message.data,
|
||||||
origin: window.location.origin,
|
origin: window.location.origin,
|
||||||
@@ -120,7 +136,7 @@ function initializeFido2ContentScript() {
|
|||||||
requestId: requestId,
|
requestId: requestId,
|
||||||
},
|
},
|
||||||
(response) => {
|
(response) => {
|
||||||
if (response.error !== undefined) {
|
if (response && response.error !== undefined) {
|
||||||
return reject(response.error);
|
return reject(response.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,6 +171,12 @@ async function run() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
initializeFido2ContentScript();
|
initializeFido2ContentScript();
|
||||||
|
|
||||||
|
const port = chrome.runtime.connect({ name: "fido2ContentScriptReady" });
|
||||||
|
port.onDisconnect.addListener(() => {
|
||||||
|
// Cleanup the messenger and remove the event listener
|
||||||
|
messenger.destroy();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
run();
|
run();
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ export enum MessageType {
|
|||||||
CredentialGetRequest,
|
CredentialGetRequest,
|
||||||
CredentialGetResponse,
|
CredentialGetResponse,
|
||||||
AbortRequest,
|
AbortRequest,
|
||||||
|
DisconnectRequest,
|
||||||
|
ReconnectRequest,
|
||||||
AbortResponse,
|
AbortResponse,
|
||||||
ErrorResponse,
|
ErrorResponse,
|
||||||
}
|
}
|
||||||
@@ -60,6 +62,14 @@ export type AbortRequest = {
|
|||||||
abortedRequestId: string;
|
abortedRequestId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type DisconnectRequest = {
|
||||||
|
type: MessageType.DisconnectRequest;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ReconnectRequest = {
|
||||||
|
type: MessageType.ReconnectRequest;
|
||||||
|
};
|
||||||
|
|
||||||
export type ErrorResponse = {
|
export type ErrorResponse = {
|
||||||
type: MessageType.ErrorResponse;
|
type: MessageType.ErrorResponse;
|
||||||
error: string;
|
error: string;
|
||||||
@@ -76,5 +86,7 @@ export type Message =
|
|||||||
| CredentialGetRequest
|
| CredentialGetRequest
|
||||||
| CredentialGetResponse
|
| CredentialGetResponse
|
||||||
| AbortRequest
|
| AbortRequest
|
||||||
|
| DisconnectRequest
|
||||||
|
| ReconnectRequest
|
||||||
| AbortResponse
|
| AbortResponse
|
||||||
| ErrorResponse;
|
| ErrorResponse;
|
||||||
|
|||||||
@@ -12,6 +12,12 @@ describe("Messenger", () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// jest does not support MessageChannel
|
// jest does not support MessageChannel
|
||||||
window.MessageChannel = MockMessageChannel as any;
|
window.MessageChannel = MockMessageChannel as any;
|
||||||
|
Object.defineProperty(window, "location", {
|
||||||
|
value: {
|
||||||
|
origin: "https://bitwarden.com",
|
||||||
|
},
|
||||||
|
writable: true,
|
||||||
|
});
|
||||||
|
|
||||||
const channelPair = new TestChannelPair();
|
const channelPair = new TestChannelPair();
|
||||||
messengerA = new Messenger(channelPair.channelA);
|
messengerA = new Messenger(channelPair.channelA);
|
||||||
@@ -27,7 +33,7 @@ describe("Messenger", () => {
|
|||||||
const request = createRequest();
|
const request = createRequest();
|
||||||
messengerA.request(request);
|
messengerA.request(request);
|
||||||
|
|
||||||
const received = handlerB.recieve();
|
const received = handlerB.receive();
|
||||||
|
|
||||||
expect(received.length).toBe(1);
|
expect(received.length).toBe(1);
|
||||||
expect(received[0].message).toMatchObject(request);
|
expect(received[0].message).toMatchObject(request);
|
||||||
@@ -37,7 +43,7 @@ describe("Messenger", () => {
|
|||||||
const request = createRequest();
|
const request = createRequest();
|
||||||
const response = createResponse();
|
const response = createResponse();
|
||||||
const requestPromise = messengerA.request(request);
|
const requestPromise = messengerA.request(request);
|
||||||
const received = handlerB.recieve();
|
const received = handlerB.receive();
|
||||||
received[0].respond(response);
|
received[0].respond(response);
|
||||||
|
|
||||||
const returned = await requestPromise;
|
const returned = await requestPromise;
|
||||||
@@ -49,7 +55,7 @@ describe("Messenger", () => {
|
|||||||
const request = createRequest();
|
const request = createRequest();
|
||||||
const error = new Error("Test error");
|
const error = new Error("Test error");
|
||||||
const requestPromise = messengerA.request(request);
|
const requestPromise = messengerA.request(request);
|
||||||
const received = handlerB.recieve();
|
const received = handlerB.receive();
|
||||||
|
|
||||||
received[0].reject(error);
|
received[0].reject(error);
|
||||||
|
|
||||||
@@ -61,10 +67,60 @@ describe("Messenger", () => {
|
|||||||
messengerA.request(createRequest(), abortController);
|
messengerA.request(createRequest(), abortController);
|
||||||
abortController.abort();
|
abortController.abort();
|
||||||
|
|
||||||
const received = handlerB.recieve();
|
const received = handlerB.receive();
|
||||||
|
|
||||||
expect(received[0].abortController.signal.aborted).toBe(true);
|
expect(received[0].abortController.signal.aborted).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("destroy", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
/**
|
||||||
|
* In Jest's jsdom environment, there is an issue where event listeners are not
|
||||||
|
* triggered upon dispatching an event. This is a workaround to mock the EventTarget
|
||||||
|
*/
|
||||||
|
window.EventTarget = MockEventTarget as any;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should remove the message event listener", async () => {
|
||||||
|
const channelPair = new TestChannelPair();
|
||||||
|
const addEventListenerSpy = jest.spyOn(channelPair.channelA, "addEventListener");
|
||||||
|
const removeEventListenerSpy = jest.spyOn(channelPair.channelA, "removeEventListener");
|
||||||
|
messengerA = new Messenger(channelPair.channelA);
|
||||||
|
jest
|
||||||
|
.spyOn(messengerA as any, "sendDisconnectCommand")
|
||||||
|
.mockImplementation(() => Promise.resolve());
|
||||||
|
|
||||||
|
expect(addEventListenerSpy).toHaveBeenCalled();
|
||||||
|
|
||||||
|
await messengerA.destroy();
|
||||||
|
|
||||||
|
expect(removeEventListenerSpy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should dispatch the destroy event on messenger destruction", async () => {
|
||||||
|
const request = createRequest();
|
||||||
|
messengerA.request(request);
|
||||||
|
|
||||||
|
const dispatchEventSpy = jest.spyOn((messengerA as any).onDestroy, "dispatchEvent");
|
||||||
|
messengerA.destroy();
|
||||||
|
|
||||||
|
expect(dispatchEventSpy).toHaveBeenCalledWith(expect.any(Event));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should trigger onDestroyListener when the destroy event is dispatched", async () => {
|
||||||
|
const request = createRequest();
|
||||||
|
messengerA.request(request);
|
||||||
|
|
||||||
|
const onDestroyListener = jest.fn();
|
||||||
|
(messengerA as any).onDestroy.addEventListener("destroy", onDestroyListener);
|
||||||
|
messengerA.destroy();
|
||||||
|
|
||||||
|
expect(onDestroyListener).toHaveBeenCalled();
|
||||||
|
const eventArg = onDestroyListener.mock.calls[0][0];
|
||||||
|
expect(eventArg).toBeInstanceOf(Event);
|
||||||
|
expect(eventArg.type).toBe("destroy");
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
type TestMessage = MessageWithMetadata & { testId: string };
|
type TestMessage = MessageWithMetadata & { testId: string };
|
||||||
@@ -86,11 +142,13 @@ class TestChannelPair {
|
|||||||
|
|
||||||
this.channelA = {
|
this.channelA = {
|
||||||
addEventListener: (listener) => (broadcastChannel.port1.onmessage = listener),
|
addEventListener: (listener) => (broadcastChannel.port1.onmessage = listener),
|
||||||
|
removeEventListener: () => (broadcastChannel.port1.onmessage = null),
|
||||||
postMessage: (message, port) => broadcastChannel.port1.postMessage(message, port),
|
postMessage: (message, port) => broadcastChannel.port1.postMessage(message, port),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.channelB = {
|
this.channelB = {
|
||||||
addEventListener: (listener) => (broadcastChannel.port2.onmessage = listener),
|
addEventListener: (listener) => (broadcastChannel.port2.onmessage = listener),
|
||||||
|
removeEventListener: () => (broadcastChannel.port1.onmessage = null),
|
||||||
postMessage: (message, port) => broadcastChannel.port2.postMessage(message, port),
|
postMessage: (message, port) => broadcastChannel.port2.postMessage(message, port),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -102,7 +160,7 @@ class TestMessageHandler {
|
|||||||
abortController?: AbortController,
|
abortController?: AbortController,
|
||||||
) => Promise<Message | undefined>;
|
) => Promise<Message | undefined>;
|
||||||
|
|
||||||
private recievedMessages: {
|
private receivedMessages: {
|
||||||
message: TestMessage;
|
message: TestMessage;
|
||||||
respond: (response: TestMessage) => void;
|
respond: (response: TestMessage) => void;
|
||||||
reject: (error: Error) => void;
|
reject: (error: Error) => void;
|
||||||
@@ -112,7 +170,7 @@ class TestMessageHandler {
|
|||||||
constructor() {
|
constructor() {
|
||||||
this.handler = (message, abortController) =>
|
this.handler = (message, abortController) =>
|
||||||
new Promise((resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
this.recievedMessages.push({
|
this.receivedMessages.push({
|
||||||
message,
|
message,
|
||||||
abortController,
|
abortController,
|
||||||
respond: (response) => resolve(response),
|
respond: (response) => resolve(response),
|
||||||
@@ -121,9 +179,9 @@ class TestMessageHandler {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
recieve() {
|
receive() {
|
||||||
const received = this.recievedMessages;
|
const received = this.receivedMessages;
|
||||||
this.recievedMessages = [];
|
this.receivedMessages = [];
|
||||||
return received;
|
return received;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -144,7 +202,11 @@ class MockMessagePort<T> {
|
|||||||
|
|
||||||
postMessage(message: T, port?: MessagePort) {
|
postMessage(message: T, port?: MessagePort) {
|
||||||
this.remotePort.onmessage(
|
this.remotePort.onmessage(
|
||||||
new MessageEvent("message", { data: message, ports: port ? [port] : [] }),
|
new MessageEvent("message", {
|
||||||
|
data: message,
|
||||||
|
ports: port ? [port] : [],
|
||||||
|
origin: "https://bitwarden.com",
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,3 +214,20 @@ class MockMessagePort<T> {
|
|||||||
// Do nothing
|
// Do nothing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class MockEventTarget {
|
||||||
|
listeners: Record<string, EventListener[]> = {};
|
||||||
|
|
||||||
|
addEventListener(type: string, callback: EventListener) {
|
||||||
|
this.listeners[type] = this.listeners[type] || [];
|
||||||
|
this.listeners[type].push(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatchEvent(event: Event) {
|
||||||
|
(this.listeners[event.type] || []).forEach((callback) => callback(event));
|
||||||
|
}
|
||||||
|
|
||||||
|
removeEventListener(type: string, callback: EventListener) {
|
||||||
|
this.listeners[type] = (this.listeners[type] || []).filter((listener) => listener !== callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { FallbackRequestedError } from "@bitwarden/common/vault/abstractions/fido2/fido2-client.service.abstraction";
|
||||||
|
|
||||||
import { Message, MessageType } from "./message";
|
import { Message, MessageType } from "./message";
|
||||||
|
|
||||||
const SENDER = "bitwarden-webauthn";
|
const SENDER = "bitwarden-webauthn";
|
||||||
@@ -6,15 +8,16 @@ type PostMessageFunction = (message: MessageWithMetadata, remotePort: MessagePor
|
|||||||
|
|
||||||
export type Channel = {
|
export type Channel = {
|
||||||
addEventListener: (listener: (message: MessageEvent<MessageWithMetadata>) => void) => void;
|
addEventListener: (listener: (message: MessageEvent<MessageWithMetadata>) => void) => void;
|
||||||
|
removeEventListener: (listener: (message: MessageEvent<MessageWithMetadata>) => void) => void;
|
||||||
postMessage: PostMessageFunction;
|
postMessage: PostMessageFunction;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Metadata = { SENDER: typeof SENDER };
|
export type Metadata = { SENDER: typeof SENDER; senderId: string };
|
||||||
export type MessageWithMetadata = Message & Metadata;
|
export type MessageWithMetadata = Message & Metadata;
|
||||||
type Handler = (
|
type Handler = (
|
||||||
message: MessageWithMetadata,
|
message: MessageWithMetadata,
|
||||||
abortController?: AbortController,
|
abortController?: AbortController,
|
||||||
) => Promise<Message | undefined>;
|
) => void | Promise<Message | undefined>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A class that handles communication between the page and content script. It converts
|
* A class that handles communication between the page and content script. It converts
|
||||||
@@ -22,6 +25,9 @@ type Handler = (
|
|||||||
* handling aborts and exceptions across separate execution contexts.
|
* handling aborts and exceptions across separate execution contexts.
|
||||||
*/
|
*/
|
||||||
export class Messenger {
|
export class Messenger {
|
||||||
|
private messageEventListener: (event: MessageEvent<MessageWithMetadata>) => void | null = null;
|
||||||
|
private onDestroy = new EventTarget();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a messenger that uses the browser's `window.postMessage` API to initiate
|
* Creates a messenger that uses the browser's `window.postMessage` API to initiate
|
||||||
* requests in the content script. Every request will then create it's own
|
* requests in the content script. Every request will then create it's own
|
||||||
@@ -35,14 +41,8 @@ export class Messenger {
|
|||||||
|
|
||||||
return new Messenger({
|
return new Messenger({
|
||||||
postMessage: (message, port) => window.postMessage(message, windowOrigin, [port]),
|
postMessage: (message, port) => window.postMessage(message, windowOrigin, [port]),
|
||||||
addEventListener: (listener) =>
|
addEventListener: (listener) => window.addEventListener("message", listener),
|
||||||
window.addEventListener("message", (event: MessageEvent<unknown>) => {
|
removeEventListener: (listener) => window.removeEventListener("message", listener),
|
||||||
if (event.origin !== windowOrigin) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
listener(event as MessageEvent<MessageWithMetadata>);
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,38 +53,11 @@ export class Messenger {
|
|||||||
*/
|
*/
|
||||||
handler?: Handler;
|
handler?: Handler;
|
||||||
|
|
||||||
|
private messengerId = this.generateUniqueId();
|
||||||
|
|
||||||
constructor(private broadcastChannel: Channel) {
|
constructor(private broadcastChannel: Channel) {
|
||||||
this.broadcastChannel.addEventListener(async (event) => {
|
this.messageEventListener = this.createMessageEventListener();
|
||||||
if (this.handler === undefined) {
|
this.broadcastChannel.addEventListener(this.messageEventListener);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const message = event.data;
|
|
||||||
const port = event.ports?.[0];
|
|
||||||
if (message?.SENDER !== SENDER || message == null || port == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const abortController = new AbortController();
|
|
||||||
port.onmessage = (event: MessageEvent<MessageWithMetadata>) => {
|
|
||||||
if (event.data.type === MessageType.AbortRequest) {
|
|
||||||
abortController.abort();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const handlerResponse = await this.handler(message, abortController);
|
|
||||||
port.postMessage({ ...handlerResponse, SENDER });
|
|
||||||
} catch (error) {
|
|
||||||
port.postMessage({
|
|
||||||
SENDER,
|
|
||||||
type: MessageType.ErrorResponse,
|
|
||||||
error: JSON.stringify(error, Object.getOwnPropertyNames(error)),
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
port.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -111,7 +84,10 @@ export class Messenger {
|
|||||||
});
|
});
|
||||||
abortController?.signal.addEventListener("abort", abortListener);
|
abortController?.signal.addEventListener("abort", abortListener);
|
||||||
|
|
||||||
this.broadcastChannel.postMessage({ ...request, SENDER }, remotePort);
|
this.broadcastChannel.postMessage(
|
||||||
|
{ ...request, SENDER, senderId: this.messengerId },
|
||||||
|
remotePort,
|
||||||
|
);
|
||||||
const response = await promise;
|
const response = await promise;
|
||||||
|
|
||||||
abortController?.signal.removeEventListener("abort", abortListener);
|
abortController?.signal.removeEventListener("abort", abortListener);
|
||||||
@@ -127,4 +103,79 @@ export class Messenger {
|
|||||||
localPort.close();
|
localPort.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private createMessageEventListener() {
|
||||||
|
return async (event: MessageEvent<MessageWithMetadata>) => {
|
||||||
|
const windowOrigin = window.location.origin;
|
||||||
|
if (event.origin !== windowOrigin || !this.handler) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = event.data;
|
||||||
|
const port = event.ports?.[0];
|
||||||
|
if (
|
||||||
|
message?.SENDER !== SENDER ||
|
||||||
|
message.senderId == this.messengerId ||
|
||||||
|
message == null ||
|
||||||
|
port == null
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const abortController = new AbortController();
|
||||||
|
port.onmessage = (event: MessageEvent<MessageWithMetadata>) => {
|
||||||
|
if (event.data.type === MessageType.AbortRequest) {
|
||||||
|
abortController.abort();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let onDestroyListener;
|
||||||
|
const destroyPromise: Promise<never> = new Promise((_, reject) => {
|
||||||
|
onDestroyListener = () => reject(new FallbackRequestedError());
|
||||||
|
this.onDestroy.addEventListener("destroy", onDestroyListener);
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const handlerResponse = await Promise.race([
|
||||||
|
this.handler(message, abortController),
|
||||||
|
destroyPromise,
|
||||||
|
]);
|
||||||
|
port.postMessage({ ...handlerResponse, SENDER });
|
||||||
|
} catch (error) {
|
||||||
|
port.postMessage({
|
||||||
|
SENDER,
|
||||||
|
type: MessageType.ErrorResponse,
|
||||||
|
error: JSON.stringify(error, Object.getOwnPropertyNames(error)),
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
this.onDestroy.removeEventListener("destroy", onDestroyListener);
|
||||||
|
port.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleans up the messenger by removing the message event listener
|
||||||
|
*/
|
||||||
|
async destroy() {
|
||||||
|
this.onDestroy.dispatchEvent(new Event("destroy"));
|
||||||
|
|
||||||
|
if (this.messageEventListener) {
|
||||||
|
await this.sendDisconnectCommand();
|
||||||
|
this.broadcastChannel.removeEventListener(this.messageEventListener);
|
||||||
|
this.messageEventListener = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendReconnectCommand() {
|
||||||
|
await this.request({ type: MessageType.ReconnectRequest });
|
||||||
|
}
|
||||||
|
|
||||||
|
private async sendDisconnectCommand() {
|
||||||
|
await this.request({ type: MessageType.DisconnectRequest });
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateUniqueId() {
|
||||||
|
return Date.now().toString(36) + Math.random().toString(36).substring(2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,10 +53,21 @@ const browserCredentials = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const messenger = ((window as any).messenger = Messenger.forDOMCommunication(window));
|
const messenger = ((window as any).messenger = Messenger.forDOMCommunication(window));
|
||||||
navigator.credentials.create = async (
|
|
||||||
|
navigator.credentials.create = createWebAuthnCredential;
|
||||||
|
navigator.credentials.get = getWebAuthnCredential;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new webauthn credential.
|
||||||
|
*
|
||||||
|
* @param options Options for creating new credentials.
|
||||||
|
* @param abortController Abort controller to abort the request if needed.
|
||||||
|
* @returns Promise that resolves to the new credential object.
|
||||||
|
*/
|
||||||
|
async function createWebAuthnCredential(
|
||||||
options?: CredentialCreationOptions,
|
options?: CredentialCreationOptions,
|
||||||
abortController?: AbortController,
|
abortController?: AbortController,
|
||||||
): Promise<Credential> => {
|
): Promise<Credential> {
|
||||||
if (!isWebauthnCall(options)) {
|
if (!isWebauthnCall(options)) {
|
||||||
return await browserCredentials.create(options);
|
return await browserCredentials.create(options);
|
||||||
}
|
}
|
||||||
@@ -88,12 +99,19 @@ navigator.credentials.create = async (
|
|||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
navigator.credentials.get = async (
|
/**
|
||||||
|
* Retrieves a webauthn credential.
|
||||||
|
*
|
||||||
|
* @param options Options for creating new credentials.
|
||||||
|
* @param abortController Abort controller to abort the request if needed.
|
||||||
|
* @returns Promise that resolves to the new credential object.
|
||||||
|
*/
|
||||||
|
async function getWebAuthnCredential(
|
||||||
options?: CredentialRequestOptions,
|
options?: CredentialRequestOptions,
|
||||||
abortController?: AbortController,
|
abortController?: AbortController,
|
||||||
): Promise<Credential> => {
|
): Promise<Credential> {
|
||||||
if (!isWebauthnCall(options)) {
|
if (!isWebauthnCall(options)) {
|
||||||
return await browserCredentials.get(options);
|
return await browserCredentials.get(options);
|
||||||
}
|
}
|
||||||
@@ -126,7 +144,7 @@ navigator.credentials.get = async (
|
|||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
function isWebauthnCall(options?: CredentialCreationOptions | CredentialRequestOptions) {
|
function isWebauthnCall(options?: CredentialCreationOptions | CredentialRequestOptions) {
|
||||||
return options && "publicKey" in options;
|
return options && "publicKey" in options;
|
||||||
@@ -174,3 +192,23 @@ async function waitForFocus(fallbackWait = 500, timeout = 5 * 60 * 1000) {
|
|||||||
window.clearTimeout(timeoutId);
|
window.clearTimeout(timeoutId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up a listener to handle cleanup or reconnection when the extension's
|
||||||
|
* context changes due to being reloaded or unloaded.
|
||||||
|
*/
|
||||||
|
messenger.handler = (message, abortController) => {
|
||||||
|
const type = message.type;
|
||||||
|
|
||||||
|
// Handle cleanup for disconnect request
|
||||||
|
if (type === MessageType.DisconnectRequest && browserNativeWebauthnSupport) {
|
||||||
|
navigator.credentials.create = browserCredentials.create;
|
||||||
|
navigator.credentials.get = browserCredentials.get;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle reinitialization for reconnect request
|
||||||
|
if (type === MessageType.ReconnectRequest && browserNativeWebauthnSupport) {
|
||||||
|
navigator.credentials.create = createWebAuthnCredential;
|
||||||
|
navigator.credentials.get = getWebAuthnCredential;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
describe("TriggerFido2ContentScriptInjection", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetModules();
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("init", () => {
|
||||||
|
it("sends a message to the extension background", () => {
|
||||||
|
require("../content/trigger-fido2-content-script-injection");
|
||||||
|
|
||||||
|
expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({
|
||||||
|
command: "triggerFido2ContentScriptInjection",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
(function () {
|
||||||
|
chrome.runtime.sendMessage({ command: "triggerFido2ContentScriptInjection" });
|
||||||
|
})();
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export abstract class Fido2Service {
|
||||||
|
init: () => Promise<void>;
|
||||||
|
injectFido2ContentScripts: (sender: chrome.runtime.MessageSender) => Promise<void>;
|
||||||
|
}
|
||||||
35
apps/browser/src/vault/services/fido2.service.spec.ts
Normal file
35
apps/browser/src/vault/services/fido2.service.spec.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||||
|
|
||||||
|
import Fido2Service from "./fido2.service";
|
||||||
|
|
||||||
|
describe("Fido2Service", () => {
|
||||||
|
let fido2Service: Fido2Service;
|
||||||
|
let tabMock: chrome.tabs.Tab;
|
||||||
|
let sender: chrome.runtime.MessageSender;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fido2Service = new Fido2Service();
|
||||||
|
tabMock = { id: 123, url: "https://bitwarden.com" } as chrome.tabs.Tab;
|
||||||
|
sender = { tab: tabMock };
|
||||||
|
jest.spyOn(BrowserApi, "executeScriptInTab").mockImplementation();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetModules();
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("injectFido2ContentScripts", () => {
|
||||||
|
const fido2ContentScript = "content/fido2/content-script.js";
|
||||||
|
const defaultExecuteScriptOptions = { runAt: "document_start" };
|
||||||
|
|
||||||
|
it("accepts an extension message sender and injects the fido2 scripts into the tab of the sender", async () => {
|
||||||
|
await fido2Service.injectFido2ContentScripts(sender);
|
||||||
|
|
||||||
|
expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith(tabMock.id, {
|
||||||
|
file: fido2ContentScript,
|
||||||
|
...defaultExecuteScriptOptions,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
33
apps/browser/src/vault/services/fido2.service.ts
Normal file
33
apps/browser/src/vault/services/fido2.service.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||||
|
|
||||||
|
import { Fido2Service as Fido2ServiceInterface } from "./abstractions/fido2.service";
|
||||||
|
|
||||||
|
export default class Fido2Service implements Fido2ServiceInterface {
|
||||||
|
async init() {
|
||||||
|
const tabs = await BrowserApi.tabsQuery({});
|
||||||
|
tabs.forEach((tab) => {
|
||||||
|
if (tab.url?.startsWith("https")) {
|
||||||
|
this.injectFido2ContentScripts({ tab } as chrome.runtime.MessageSender);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
BrowserApi.addListener(chrome.runtime.onConnect, (port) => {
|
||||||
|
if (port.name === "fido2ContentScriptReady") {
|
||||||
|
port.postMessage({ command: "fido2ContentScriptInit" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injects the FIDO2 content script into the current tab.
|
||||||
|
* @param {chrome.runtime.MessageSender} sender
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async injectFido2ContentScripts(sender: chrome.runtime.MessageSender): Promise<void> {
|
||||||
|
await BrowserApi.executeScriptInTab(sender.tab.id, {
|
||||||
|
file: "content/fido2/content-script.js",
|
||||||
|
frameId: sender.frameId,
|
||||||
|
runAt: "document_start",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -171,6 +171,8 @@ const mainConfig = {
|
|||||||
"content/notificationBar": "./src/autofill/content/notification-bar.ts",
|
"content/notificationBar": "./src/autofill/content/notification-bar.ts",
|
||||||
"content/contextMenuHandler": "./src/autofill/content/context-menu-handler.ts",
|
"content/contextMenuHandler": "./src/autofill/content/context-menu-handler.ts",
|
||||||
"content/message_handler": "./src/autofill/content/message_handler.ts",
|
"content/message_handler": "./src/autofill/content/message_handler.ts",
|
||||||
|
"content/fido2/trigger-fido2-content-script-injection":
|
||||||
|
"./src/vault/fido2/content/trigger-fido2-content-script-injection.ts",
|
||||||
"content/fido2/content-script": "./src/vault/fido2/content/content-script.ts",
|
"content/fido2/content-script": "./src/vault/fido2/content/content-script.ts",
|
||||||
"content/fido2/page-script": "./src/vault/fido2/content/page-script.ts",
|
"content/fido2/page-script": "./src/vault/fido2/content/page-script.ts",
|
||||||
"notification/bar": "./src/autofill/notification/bar.ts",
|
"notification/bar": "./src/autofill/notification/bar.ts",
|
||||||
|
|||||||
@@ -232,8 +232,10 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
case "syncStarted":
|
case "syncStarted":
|
||||||
break;
|
break;
|
||||||
case "syncCompleted":
|
case "syncCompleted":
|
||||||
await this.updateAppMenu();
|
if (message.successfully) {
|
||||||
this.configService.triggerServerConfigFetch();
|
this.updateAppMenu();
|
||||||
|
this.configService.triggerServerConfigFetch();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case "openSettings":
|
case "openSettings":
|
||||||
await this.openModal<SettingsComponent>(SettingsComponent, this.settingsRef);
|
await this.openModal<SettingsComponent>(SettingsComponent, this.settingsRef);
|
||||||
|
|||||||
@@ -200,7 +200,7 @@
|
|||||||
<input
|
<input
|
||||||
id="length"
|
id="length"
|
||||||
type="number"
|
type="number"
|
||||||
min="5"
|
[min]="passwordOptions.minLength"
|
||||||
max="128"
|
max="128"
|
||||||
[(ngModel)]="passwordOptions.length"
|
[(ngModel)]="passwordOptions.length"
|
||||||
(blur)="savePasswordOptions()"
|
(blur)="savePasswordOptions()"
|
||||||
@@ -208,7 +208,7 @@
|
|||||||
<input
|
<input
|
||||||
id="lengthRange"
|
id="lengthRange"
|
||||||
type="range"
|
type="range"
|
||||||
min="5"
|
[min]="passwordOptions.minLength"
|
||||||
max="128"
|
max="128"
|
||||||
step="1"
|
step="1"
|
||||||
[(ngModel)]="passwordOptions.length"
|
[(ngModel)]="passwordOptions.length"
|
||||||
@@ -218,6 +218,18 @@
|
|||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="box-content-row" appBoxRow>
|
||||||
|
<span>{{ "passwordMinLength" | i18n }}</span>
|
||||||
|
<span class="txt-right">{{ passwordOptions.minLength }}</span>
|
||||||
|
<span
|
||||||
|
class="sr-only"
|
||||||
|
attr.aria-label="{{ 'passwordMinLength' | i18n }}"
|
||||||
|
role="status"
|
||||||
|
aria-live="polite"
|
||||||
|
>
|
||||||
|
{{ passwordOptionsMinLengthForReader$ | async }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
||||||
<label for="uppercase">A-Z</label>
|
<label for="uppercase">A-Z</label>
|
||||||
<input
|
<input
|
||||||
@@ -247,7 +259,8 @@
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
(change)="savePasswordOptions()"
|
(change)="savePasswordOptions()"
|
||||||
[disabled]="enforcedPasswordPolicyOptions?.useNumbers"
|
[disabled]="enforcedPasswordPolicyOptions?.useNumbers"
|
||||||
[(ngModel)]="passwordOptions.number"
|
[ngModel]="passwordOptions.number"
|
||||||
|
(ngModelChange)="setPasswordOptionsNumber($event)"
|
||||||
attr.aria-label="{{ 'numbers' | i18n }}"
|
attr.aria-label="{{ 'numbers' | i18n }}"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -258,7 +271,8 @@
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
(change)="savePasswordOptions()"
|
(change)="savePasswordOptions()"
|
||||||
[disabled]="enforcedPasswordPolicyOptions?.useSpecial"
|
[disabled]="enforcedPasswordPolicyOptions?.useSpecial"
|
||||||
[(ngModel)]="passwordOptions.special"
|
[ngModel]="passwordOptions.special"
|
||||||
|
(ngModelChange)="setPasswordOptionsSpecial($event)"
|
||||||
attr.aria-label="{{ 'specialCharacters' | i18n }}"
|
attr.aria-label="{{ 'specialCharacters' | i18n }}"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -275,6 +289,7 @@
|
|||||||
max="9"
|
max="9"
|
||||||
(change)="savePasswordOptions()"
|
(change)="savePasswordOptions()"
|
||||||
[(ngModel)]="passwordOptions.minNumber"
|
[(ngModel)]="passwordOptions.minNumber"
|
||||||
|
(input)="onPasswordOptionsMinNumberInput($event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="box-content-row box-content-row-input" appBoxRow>
|
<div class="box-content-row box-content-row-input" appBoxRow>
|
||||||
@@ -286,6 +301,7 @@
|
|||||||
max="9"
|
max="9"
|
||||||
(change)="savePasswordOptions()"
|
(change)="savePasswordOptions()"
|
||||||
[(ngModel)]="passwordOptions.minSpecial"
|
[(ngModel)]="passwordOptions.minSpecial"
|
||||||
|
(input)="onPasswordOptionsMinSpecialInput($event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
||||||
|
|||||||
@@ -403,6 +403,9 @@
|
|||||||
"length": {
|
"length": {
|
||||||
"message": "Length"
|
"message": "Length"
|
||||||
},
|
},
|
||||||
|
"passwordMinLength": {
|
||||||
|
"message": "Minimum password length"
|
||||||
|
},
|
||||||
"uppercase": {
|
"uppercase": {
|
||||||
"message": "Uppercase (A-Z)"
|
"message": "Uppercase (A-Z)"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -217,7 +217,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.img-right {
|
.img-right,
|
||||||
|
.txt-right {
|
||||||
float: right;
|
float: right;
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -134,7 +134,9 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||||||
case "syncStarted":
|
case "syncStarted":
|
||||||
break;
|
break;
|
||||||
case "syncCompleted":
|
case "syncCompleted":
|
||||||
this.configService.triggerServerConfigFetch();
|
if (message.successfully) {
|
||||||
|
this.configService.triggerServerConfigFetch();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case "upgradeOrganization": {
|
case "upgradeOrganization": {
|
||||||
const upgradeConfirmed = await this.dialogService.openSimpleDialog({
|
const upgradeConfirmed = await this.dialogService.openSimpleDialog({
|
||||||
|
|||||||
@@ -109,13 +109,31 @@
|
|||||||
id="length"
|
id="length"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
type="number"
|
type="number"
|
||||||
min="5"
|
[min]="passwordOptions.minLength"
|
||||||
max="128"
|
max="128"
|
||||||
[(ngModel)]="passwordOptions.length"
|
[(ngModel)]="passwordOptions.length"
|
||||||
(blur)="savePasswordOptions()"
|
(blur)="savePasswordOptions()"
|
||||||
(change)="lengthChanged()"
|
(change)="lengthChanged()"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group col-4">
|
||||||
|
<label for="min-length">{{ "passwordMinLength" | i18n }}</label>
|
||||||
|
<input
|
||||||
|
id="min-length"
|
||||||
|
class="form-control"
|
||||||
|
type="text"
|
||||||
|
readonly="true"
|
||||||
|
[value]="passwordOptions.length"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
class="sr-only"
|
||||||
|
attr.aria-label="{{ 'passwordMinLength' | i18n }}"
|
||||||
|
role="status"
|
||||||
|
aria-live="polite"
|
||||||
|
>
|
||||||
|
{{ passwordOptionsMinLengthForReader$ | async }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<div class="form-group col-4">
|
<div class="form-group col-4">
|
||||||
<label for="min-number">{{ "minNumbers" | i18n }}</label>
|
<label for="min-number">{{ "minNumbers" | i18n }}</label>
|
||||||
<input
|
<input
|
||||||
@@ -124,8 +142,8 @@
|
|||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
max="9"
|
max="9"
|
||||||
(blur)="savePasswordOptions()"
|
|
||||||
[(ngModel)]="passwordOptions.minNumber"
|
[(ngModel)]="passwordOptions.minNumber"
|
||||||
|
(input)="onPasswordOptionsMinNumberInput($event)"
|
||||||
(change)="minNumberChanged()"
|
(change)="minNumberChanged()"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -137,8 +155,8 @@
|
|||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
max="9"
|
max="9"
|
||||||
(blur)="savePasswordOptions()"
|
|
||||||
[(ngModel)]="passwordOptions.minSpecial"
|
[(ngModel)]="passwordOptions.minSpecial"
|
||||||
|
(input)="onPasswordOptionsMinSpecialInput($event)"
|
||||||
(change)="minSpecialChanged()"
|
(change)="minSpecialChanged()"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -175,7 +193,8 @@
|
|||||||
class="form-check-input"
|
class="form-check-input"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
(change)="savePasswordOptions()"
|
(change)="savePasswordOptions()"
|
||||||
[(ngModel)]="passwordOptions.number"
|
[ngModel]="passwordOptions.number"
|
||||||
|
(ngModelChange)="setPasswordOptionsNumber($event)"
|
||||||
[disabled]="enforcedPasswordPolicyOptions?.useNumbers"
|
[disabled]="enforcedPasswordPolicyOptions?.useNumbers"
|
||||||
attr.aria-label="{{ 'numbers' | i18n }}"
|
attr.aria-label="{{ 'numbers' | i18n }}"
|
||||||
/>
|
/>
|
||||||
@@ -186,8 +205,8 @@
|
|||||||
id="special"
|
id="special"
|
||||||
class="form-check-input"
|
class="form-check-input"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
(change)="savePasswordOptions()"
|
[ngModel]="passwordOptions.special"
|
||||||
[(ngModel)]="passwordOptions.special"
|
(ngModelChange)="setPasswordOptionsSpecial($event)"
|
||||||
[disabled]="enforcedPasswordPolicyOptions?.useSpecial"
|
[disabled]="enforcedPasswordPolicyOptions?.useSpecial"
|
||||||
attr.aria-label="{{ 'specialCharacters' | i18n }}"
|
attr.aria-label="{{ 'specialCharacters' | i18n }}"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1150,6 +1150,9 @@
|
|||||||
"length": {
|
"length": {
|
||||||
"message": "Length"
|
"message": "Length"
|
||||||
},
|
},
|
||||||
|
"passwordMinLength": {
|
||||||
|
"message": "Minimum password length"
|
||||||
|
},
|
||||||
"uppercase": {
|
"uppercase": {
|
||||||
"message": "Uppercase (A-Z)",
|
"message": "Uppercase (A-Z)",
|
||||||
"description": "Include uppercase letters in the password generator."
|
"description": "Include uppercase letters in the password generator."
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||||
import { ActivatedRoute } from "@angular/router";
|
import { ActivatedRoute } from "@angular/router";
|
||||||
import { first } from "rxjs/operators";
|
import { BehaviorSubject } from "rxjs";
|
||||||
|
import { debounceTime, first, map } from "rxjs/operators";
|
||||||
|
|
||||||
import { PasswordGeneratorPolicyOptions } from "@bitwarden/common/admin-console/models/domain/password-generator-policy-options";
|
import { PasswordGeneratorPolicyOptions } from "@bitwarden/common/admin-console/models/domain/password-generator-policy-options";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
@@ -12,6 +13,7 @@ import {
|
|||||||
PasswordGenerationServiceAbstraction,
|
PasswordGenerationServiceAbstraction,
|
||||||
PasswordGeneratorOptions,
|
PasswordGeneratorOptions,
|
||||||
} from "@bitwarden/common/tools/generator/password";
|
} from "@bitwarden/common/tools/generator/password";
|
||||||
|
import { DefaultBoundaries } from "@bitwarden/common/tools/generator/password/password-generator-options-evaluator";
|
||||||
import {
|
import {
|
||||||
UsernameGenerationServiceAbstraction,
|
UsernameGenerationServiceAbstraction,
|
||||||
UsernameGeneratorOptions,
|
UsernameGeneratorOptions,
|
||||||
@@ -40,6 +42,16 @@ export class GeneratorComponent implements OnInit {
|
|||||||
enforcedPasswordPolicyOptions: PasswordGeneratorPolicyOptions;
|
enforcedPasswordPolicyOptions: PasswordGeneratorPolicyOptions;
|
||||||
usernameWebsite: string = null;
|
usernameWebsite: string = null;
|
||||||
|
|
||||||
|
// update screen reader minimum password length with 500ms debounce
|
||||||
|
// so that the user isn't flooded with status updates
|
||||||
|
private _passwordOptionsMinLengthForReader = new BehaviorSubject<number>(
|
||||||
|
DefaultBoundaries.length.min,
|
||||||
|
);
|
||||||
|
protected passwordOptionsMinLengthForReader$ = this._passwordOptionsMinLengthForReader.pipe(
|
||||||
|
map((val) => val || DefaultBoundaries.length.min),
|
||||||
|
debounceTime(500),
|
||||||
|
);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected passwordGenerationService: PasswordGenerationServiceAbstraction,
|
protected passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||||
protected usernameGenerationService: UsernameGenerationServiceAbstraction,
|
protected usernameGenerationService: UsernameGenerationServiceAbstraction,
|
||||||
@@ -144,6 +156,44 @@ export class GeneratorComponent implements OnInit {
|
|||||||
await this.passwordGenerationService.addHistory(this.password);
|
await this.passwordGenerationService.addHistory(this.password);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async onPasswordOptionsMinNumberInput($event: Event) {
|
||||||
|
// `savePasswordOptions()` replaces the null
|
||||||
|
this.passwordOptions.number = null;
|
||||||
|
|
||||||
|
await this.savePasswordOptions();
|
||||||
|
|
||||||
|
// fixes UI desync that occurs when minNumber has a fixed value
|
||||||
|
// that is reset through normalization
|
||||||
|
($event.target as HTMLInputElement).value = `${this.passwordOptions.minNumber}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setPasswordOptionsNumber($event: boolean) {
|
||||||
|
this.passwordOptions.number = $event;
|
||||||
|
// `savePasswordOptions()` replaces the null
|
||||||
|
this.passwordOptions.minNumber = null;
|
||||||
|
|
||||||
|
await this.savePasswordOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
async onPasswordOptionsMinSpecialInput($event: Event) {
|
||||||
|
// `savePasswordOptions()` replaces the null
|
||||||
|
this.passwordOptions.special = null;
|
||||||
|
|
||||||
|
await this.savePasswordOptions();
|
||||||
|
|
||||||
|
// fixes UI desync that occurs when minSpecial has a fixed value
|
||||||
|
// that is reset through normalization
|
||||||
|
($event.target as HTMLInputElement).value = `${this.passwordOptions.minSpecial}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setPasswordOptionsSpecial($event: boolean) {
|
||||||
|
this.passwordOptions.special = $event;
|
||||||
|
// `savePasswordOptions()` replaces the null
|
||||||
|
this.passwordOptions.minSpecial = null;
|
||||||
|
|
||||||
|
await this.savePasswordOptions();
|
||||||
|
}
|
||||||
|
|
||||||
async sliderInput() {
|
async sliderInput() {
|
||||||
this.normalizePasswordOptions();
|
this.normalizePasswordOptions();
|
||||||
this.password = await this.passwordGenerationService.generatePassword(this.passwordOptions);
|
this.password = await this.passwordGenerationService.generatePassword(this.passwordOptions);
|
||||||
@@ -240,6 +290,8 @@ export class GeneratorComponent implements OnInit {
|
|||||||
this.passwordOptions,
|
this.passwordOptions,
|
||||||
this.enforcedPasswordPolicyOptions,
|
this.enforcedPasswordPolicyOptions,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this._passwordOptionsMinLengthForReader.next(this.passwordOptions.minLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async initForwardOptions() {
|
private async initForwardOptions() {
|
||||||
|
|||||||
@@ -1,18 +1,73 @@
|
|||||||
import Domain from "../../../platform/models/domain/domain-base";
|
import Domain from "../../../platform/models/domain/domain-base";
|
||||||
|
|
||||||
|
/** Enterprise policy for the password generator.
|
||||||
|
* @see PolicyType.PasswordGenerator
|
||||||
|
*/
|
||||||
export class PasswordGeneratorPolicyOptions extends Domain {
|
export class PasswordGeneratorPolicyOptions extends Domain {
|
||||||
defaultType = "";
|
/** The default kind of credential to generate */
|
||||||
|
defaultType: "password" | "passphrase" | "" = "";
|
||||||
|
|
||||||
|
/** The minimum length of generated passwords.
|
||||||
|
* When this is less than or equal to zero, it is ignored.
|
||||||
|
* If this is less than the total number of characters required by
|
||||||
|
* the policy's other settings, then it is ignored.
|
||||||
|
* This field is not used for passphrases.
|
||||||
|
*/
|
||||||
minLength = 0;
|
minLength = 0;
|
||||||
|
|
||||||
|
/** When this is true, an uppercase character must be part of
|
||||||
|
* the generated password.
|
||||||
|
* This field is not used for passphrases.
|
||||||
|
*/
|
||||||
useUppercase = false;
|
useUppercase = false;
|
||||||
|
|
||||||
|
/** When this is true, a lowercase character must be part of
|
||||||
|
* the generated password. This field is not used for passphrases.
|
||||||
|
*/
|
||||||
useLowercase = false;
|
useLowercase = false;
|
||||||
|
|
||||||
|
/** When this is true, at least one digit must be part of the generated
|
||||||
|
* password. This field is not used for passphrases.
|
||||||
|
*/
|
||||||
useNumbers = false;
|
useNumbers = false;
|
||||||
|
|
||||||
|
/** The quantity of digits to include in the generated password.
|
||||||
|
* When this is less than or equal to zero, it is ignored.
|
||||||
|
* This field is not used for passphrases.
|
||||||
|
*/
|
||||||
numberCount = 0;
|
numberCount = 0;
|
||||||
|
|
||||||
|
/** When this is true, at least one digit must be part of the generated
|
||||||
|
* password. This field is not used for passphrases.
|
||||||
|
*/
|
||||||
useSpecial = false;
|
useSpecial = false;
|
||||||
|
|
||||||
|
/** The quantity of special characters to include in the generated
|
||||||
|
* password. When this is less than or equal to zero, it is ignored.
|
||||||
|
* This field is not used for passphrases.
|
||||||
|
*/
|
||||||
specialCount = 0;
|
specialCount = 0;
|
||||||
|
|
||||||
|
/** The minimum number of words required by generated passphrases.
|
||||||
|
* This field is not used for passwords.
|
||||||
|
*/
|
||||||
minNumberWords = 0;
|
minNumberWords = 0;
|
||||||
|
|
||||||
|
/** When this is true, the first letter of each word in the passphrase
|
||||||
|
* is capitalized. This field is not used for passwords.
|
||||||
|
*/
|
||||||
capitalize = false;
|
capitalize = false;
|
||||||
|
|
||||||
|
/** When this is true, a number is included within the passphrase.
|
||||||
|
* This field is not used for passwords.
|
||||||
|
*/
|
||||||
includeNumber = false;
|
includeNumber = false;
|
||||||
|
|
||||||
|
/** Checks whether the policy affects the password generator.
|
||||||
|
* @returns True if at least one password or passphrase requirement has been set.
|
||||||
|
* If it returns False, then no requirements have been set and the policy should
|
||||||
|
* not be enforced.
|
||||||
|
*/
|
||||||
inEffect() {
|
inEffect() {
|
||||||
return (
|
return (
|
||||||
this.defaultType !== "" ||
|
this.defaultType !== "" ||
|
||||||
@@ -28,4 +83,12 @@ export class PasswordGeneratorPolicyOptions extends Domain {
|
|||||||
this.includeNumber
|
this.includeNumber
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Creates a copy of the policy.
|
||||||
|
*/
|
||||||
|
clone() {
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
Object.assign(policy, this);
|
||||||
|
return policy;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,220 @@
|
|||||||
|
import { PasswordGeneratorPolicyOptions } from "../../../admin-console/models/domain/password-generator-policy-options";
|
||||||
|
|
||||||
|
import {
|
||||||
|
DefaultBoundaries,
|
||||||
|
PassphraseGeneratorOptionsEvaluator,
|
||||||
|
} from "./passphrase-generator-options-evaluator";
|
||||||
|
import { PassphraseGenerationOptions } from "./password-generator-options";
|
||||||
|
|
||||||
|
describe("Password generator options builder", () => {
|
||||||
|
describe("constructor()", () => {
|
||||||
|
it("should set the policy object to a copy of the input policy", () => {
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
policy.minLength = 10; // arbitrary change for deep equality check
|
||||||
|
|
||||||
|
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
|
|
||||||
|
expect(builder.policy).toEqual(policy);
|
||||||
|
expect(builder.policy).not.toBe(policy);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set default boundaries when a default policy is used", () => {
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
|
|
||||||
|
expect(builder.numWords).toEqual(DefaultBoundaries.numWords);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([1, 2])(
|
||||||
|
"should use the default word boundaries when they are greater than `policy.minNumberWords` (= %i)",
|
||||||
|
(minNumberWords) => {
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
policy.minNumberWords = minNumberWords;
|
||||||
|
|
||||||
|
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
|
|
||||||
|
expect(builder.numWords).toEqual(DefaultBoundaries.numWords);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each([8, 12, 18])(
|
||||||
|
"should use `policy.minNumberWords` (= %i) when it is greater than the default minimum words",
|
||||||
|
(minNumberWords) => {
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
policy.minNumberWords = minNumberWords;
|
||||||
|
|
||||||
|
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
|
|
||||||
|
expect(builder.numWords.min).toEqual(minNumberWords);
|
||||||
|
expect(builder.numWords.max).toEqual(DefaultBoundaries.numWords.max);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each([150, 300, 9000])(
|
||||||
|
"should use `policy.minNumberWords` (= %i) when it is greater than the default boundaries",
|
||||||
|
(minNumberWords) => {
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
policy.minNumberWords = minNumberWords;
|
||||||
|
|
||||||
|
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
|
|
||||||
|
expect(builder.numWords.min).toEqual(minNumberWords);
|
||||||
|
expect(builder.numWords.max).toEqual(minNumberWords);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("applyPolicy(options)", () => {
|
||||||
|
// All tests should freeze the options to ensure they are not modified
|
||||||
|
|
||||||
|
it("should set `capitalize` to `false` when the policy does not override it", () => {
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
|
const options = Object.freeze({});
|
||||||
|
|
||||||
|
const sanitizedOptions = builder.applyPolicy(options);
|
||||||
|
|
||||||
|
expect(sanitizedOptions.capitalize).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set `capitalize` to `true` when the policy overrides it", () => {
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
policy.capitalize = true;
|
||||||
|
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
|
const options = Object.freeze({ capitalize: false });
|
||||||
|
|
||||||
|
const sanitizedOptions = builder.applyPolicy(options);
|
||||||
|
|
||||||
|
expect(sanitizedOptions.capitalize).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set `includeNumber` to false when the policy does not override it", () => {
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
|
const options = Object.freeze({});
|
||||||
|
|
||||||
|
const sanitizedOptions = builder.applyPolicy(options);
|
||||||
|
|
||||||
|
expect(sanitizedOptions.includeNumber).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set `includeNumber` to true when the policy overrides it", () => {
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
policy.includeNumber = true;
|
||||||
|
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
|
const options = Object.freeze({ includeNumber: false });
|
||||||
|
|
||||||
|
const sanitizedOptions = builder.applyPolicy(options);
|
||||||
|
|
||||||
|
expect(sanitizedOptions.includeNumber).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set `numWords` to the minimum value when it isn't supplied", () => {
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
|
const options = Object.freeze({});
|
||||||
|
|
||||||
|
const sanitizedOptions = builder.applyPolicy(options);
|
||||||
|
|
||||||
|
expect(sanitizedOptions.numWords).toBe(builder.numWords.min);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([1, 2])(
|
||||||
|
"should set `numWords` (= %i) to the minimum value when it is less than the minimum",
|
||||||
|
(numWords) => {
|
||||||
|
expect(numWords).toBeLessThan(DefaultBoundaries.numWords.min);
|
||||||
|
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
|
const options = Object.freeze({ numWords });
|
||||||
|
|
||||||
|
const sanitizedOptions = builder.applyPolicy(options);
|
||||||
|
|
||||||
|
expect(sanitizedOptions.numWords).toBe(builder.numWords.min);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each([3, 8, 18, 20])(
|
||||||
|
"should set `numWords` (= %i) to the input value when it is within the boundaries",
|
||||||
|
(numWords) => {
|
||||||
|
expect(numWords).toBeGreaterThanOrEqual(DefaultBoundaries.numWords.min);
|
||||||
|
expect(numWords).toBeLessThanOrEqual(DefaultBoundaries.numWords.max);
|
||||||
|
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
|
const options = Object.freeze({ numWords });
|
||||||
|
|
||||||
|
const sanitizedOptions = builder.applyPolicy(options);
|
||||||
|
|
||||||
|
expect(sanitizedOptions.numWords).toBe(numWords);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each([21, 30, 50, 100])(
|
||||||
|
"should set `numWords` (= %i) to the maximum value when it is greater than the maximum",
|
||||||
|
(numWords) => {
|
||||||
|
expect(numWords).toBeGreaterThan(DefaultBoundaries.numWords.max);
|
||||||
|
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
|
const options = Object.freeze({ numWords });
|
||||||
|
|
||||||
|
const sanitizedOptions = builder.applyPolicy(options);
|
||||||
|
|
||||||
|
expect(sanitizedOptions.numWords).toBe(builder.numWords.max);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it("should preserve unknown properties", () => {
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
|
const options = Object.freeze({
|
||||||
|
unknown: "property",
|
||||||
|
another: "unknown property",
|
||||||
|
}) as PassphraseGenerationOptions;
|
||||||
|
|
||||||
|
const sanitizedOptions: any = builder.applyPolicy(options);
|
||||||
|
|
||||||
|
expect(sanitizedOptions.unknown).toEqual("property");
|
||||||
|
expect(sanitizedOptions.another).toEqual("unknown property");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("sanitize(options)", () => {
|
||||||
|
// All tests should freeze the options to ensure they are not modified
|
||||||
|
|
||||||
|
it("should return the input options without altering them", () => {
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
|
const options = Object.freeze({ wordSeparator: "%" });
|
||||||
|
|
||||||
|
const sanitizedOptions = builder.sanitize(options);
|
||||||
|
|
||||||
|
expect(sanitizedOptions).toEqual(options);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set `wordSeparator` to '-' when it isn't supplied and there is no policy override", () => {
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
|
const options = Object.freeze({});
|
||||||
|
|
||||||
|
const sanitizedOptions = builder.sanitize(options);
|
||||||
|
|
||||||
|
expect(sanitizedOptions.wordSeparator).toEqual("-");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should preserve unknown properties", () => {
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
|
const options = Object.freeze({
|
||||||
|
unknown: "property",
|
||||||
|
another: "unknown property",
|
||||||
|
}) as PassphraseGenerationOptions;
|
||||||
|
|
||||||
|
const sanitizedOptions: any = builder.sanitize(options);
|
||||||
|
|
||||||
|
expect(sanitizedOptions.unknown).toEqual("property");
|
||||||
|
expect(sanitizedOptions.another).toEqual("unknown property");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import { PasswordGeneratorPolicyOptions } from "../../../admin-console/models/domain/password-generator-policy-options";
|
||||||
|
|
||||||
|
import { PassphraseGenerationOptions } from "./password-generator-options";
|
||||||
|
|
||||||
|
type Boundary = {
|
||||||
|
readonly min: number;
|
||||||
|
readonly max: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function initializeBoundaries() {
|
||||||
|
const numWords = Object.freeze({
|
||||||
|
min: 3,
|
||||||
|
max: 20,
|
||||||
|
});
|
||||||
|
|
||||||
|
return Object.freeze({
|
||||||
|
numWords,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Immutable default boundaries for passphrase generation.
|
||||||
|
* These are used when the policy does not override a value.
|
||||||
|
*/
|
||||||
|
export const DefaultBoundaries = initializeBoundaries();
|
||||||
|
|
||||||
|
/** Enforces policy for passphrase generation options.
|
||||||
|
*/
|
||||||
|
export class PassphraseGeneratorOptionsEvaluator {
|
||||||
|
// This design is not ideal, but it is a step towards a more robust passphrase
|
||||||
|
// generator. Ideally, `sanitize` would be implemented on an options class,
|
||||||
|
// and `applyPolicy` would be implemented on a policy class, "mise en place".
|
||||||
|
//
|
||||||
|
// The current design of the passphrase generator, unfortunately, would require
|
||||||
|
// a substantial rewrite to make this feasible. Hopefully this change can be
|
||||||
|
// applied when the passphrase generator is ported to rust.
|
||||||
|
|
||||||
|
/** Policy applied by the evaluator.
|
||||||
|
*/
|
||||||
|
readonly policy: PasswordGeneratorPolicyOptions;
|
||||||
|
|
||||||
|
/** Boundaries for the number of words allowed in the password.
|
||||||
|
*/
|
||||||
|
readonly numWords: Boundary;
|
||||||
|
|
||||||
|
/** Instantiates the evaluator.
|
||||||
|
* @param policy The policy applied by the evaluator. When this conflicts with
|
||||||
|
* the defaults, the policy takes precedence.
|
||||||
|
*/
|
||||||
|
constructor(policy: PasswordGeneratorPolicyOptions) {
|
||||||
|
function createBoundary(value: number, defaultBoundary: Boundary): Boundary {
|
||||||
|
const boundary = {
|
||||||
|
min: Math.max(defaultBoundary.min, value),
|
||||||
|
max: Math.max(defaultBoundary.max, value),
|
||||||
|
};
|
||||||
|
|
||||||
|
return boundary;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.policy = policy.clone();
|
||||||
|
this.numWords = createBoundary(policy.minNumberWords, DefaultBoundaries.numWords);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Apply policy to the input options.
|
||||||
|
* @param options The options to build from. These options are not altered.
|
||||||
|
* @returns A new password generation request with policy applied.
|
||||||
|
*/
|
||||||
|
applyPolicy(options: PassphraseGenerationOptions): PassphraseGenerationOptions {
|
||||||
|
function fitToBounds(value: number, boundaries: Boundary) {
|
||||||
|
const { min, max } = boundaries;
|
||||||
|
|
||||||
|
const withUpperBound = Math.min(value ?? boundaries.min, max);
|
||||||
|
const withLowerBound = Math.max(withUpperBound, min);
|
||||||
|
|
||||||
|
return withLowerBound;
|
||||||
|
}
|
||||||
|
|
||||||
|
// apply policy overrides
|
||||||
|
const capitalize = this.policy.capitalize || options.capitalize || false;
|
||||||
|
const includeNumber = this.policy.includeNumber || options.includeNumber || false;
|
||||||
|
|
||||||
|
// apply boundaries
|
||||||
|
const numWords = fitToBounds(options.numWords, this.numWords);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...options,
|
||||||
|
numWords,
|
||||||
|
capitalize,
|
||||||
|
includeNumber,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ensures internal options consistency.
|
||||||
|
* @param options The options to cascade. These options are not altered.
|
||||||
|
* @returns A passphrase generation request with cascade applied.
|
||||||
|
*/
|
||||||
|
sanitize(options: PassphraseGenerationOptions): PassphraseGenerationOptions {
|
||||||
|
// ensure words are separated by a single character
|
||||||
|
const wordSeparator = options.wordSeparator?.[0] ?? "-";
|
||||||
|
|
||||||
|
return {
|
||||||
|
...options,
|
||||||
|
wordSeparator,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,11 +7,14 @@ import { EFFLongWordList } from "../../../platform/misc/wordlist";
|
|||||||
import { EncString } from "../../../platform/models/domain/enc-string";
|
import { EncString } from "../../../platform/models/domain/enc-string";
|
||||||
|
|
||||||
import { GeneratedPasswordHistory } from "./generated-password-history";
|
import { GeneratedPasswordHistory } from "./generated-password-history";
|
||||||
|
import { PassphraseGeneratorOptionsEvaluator } from "./passphrase-generator-options-evaluator";
|
||||||
import { PasswordGenerationServiceAbstraction } from "./password-generation.service.abstraction";
|
import { PasswordGenerationServiceAbstraction } from "./password-generation.service.abstraction";
|
||||||
import { PasswordGeneratorOptions } from "./password-generator-options";
|
import { PasswordGeneratorOptions } from "./password-generator-options";
|
||||||
|
import { PasswordGeneratorOptionsEvaluator } from "./password-generator-options-evaluator";
|
||||||
|
|
||||||
const DefaultOptions: PasswordGeneratorOptions = {
|
const DefaultOptions: PasswordGeneratorOptions = {
|
||||||
length: 14,
|
length: 14,
|
||||||
|
minLength: 5,
|
||||||
ambiguous: false,
|
ambiguous: false,
|
||||||
number: true,
|
number: true,
|
||||||
minNumber: 1,
|
minNumber: 1,
|
||||||
@@ -28,6 +31,8 @@ const DefaultOptions: PasswordGeneratorOptions = {
|
|||||||
includeNumber: false,
|
includeNumber: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const DefaultPolicy = new PasswordGeneratorPolicyOptions();
|
||||||
|
|
||||||
const MaxPasswordsInHistory = 100;
|
const MaxPasswordsInHistory = 100;
|
||||||
|
|
||||||
export class PasswordGenerationService implements PasswordGenerationServiceAbstraction {
|
export class PasswordGenerationService implements PasswordGenerationServiceAbstraction {
|
||||||
@@ -38,20 +43,12 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async generatePassword(options: PasswordGeneratorOptions): Promise<string> {
|
async generatePassword(options: PasswordGeneratorOptions): Promise<string> {
|
||||||
// overload defaults with given options
|
if ((options.type ?? DefaultOptions.type) === "passphrase") {
|
||||||
const o = Object.assign({}, DefaultOptions, options);
|
return this.generatePassphrase({ ...DefaultOptions, ...options });
|
||||||
|
|
||||||
if (o.type === "passphrase") {
|
|
||||||
return this.generatePassphrase(options);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// sanitize
|
const evaluator = new PasswordGeneratorOptionsEvaluator(DefaultPolicy);
|
||||||
this.sanitizePasswordLength(o, true);
|
const o = evaluator.sanitize({ ...DefaultOptions, ...options });
|
||||||
|
|
||||||
const minLength: number = o.minUppercase + o.minLowercase + o.minNumber + o.minSpecial;
|
|
||||||
if (o.length < minLength) {
|
|
||||||
o.length = minLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
const positions: string[] = [];
|
const positions: string[] = [];
|
||||||
if (o.lowercase && o.minLowercase > 0) {
|
if (o.lowercase && o.minLowercase > 0) {
|
||||||
@@ -144,7 +141,8 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr
|
|||||||
}
|
}
|
||||||
|
|
||||||
async generatePassphrase(options: PasswordGeneratorOptions): Promise<string> {
|
async generatePassphrase(options: PasswordGeneratorOptions): Promise<string> {
|
||||||
const o = Object.assign({}, DefaultOptions, options);
|
const evaluator = new PassphraseGeneratorOptionsEvaluator(DefaultPolicy);
|
||||||
|
const o = evaluator.sanitize({ ...DefaultOptions, ...options });
|
||||||
|
|
||||||
if (o.numWords == null || o.numWords <= 2) {
|
if (o.numWords == null || o.numWords <= 2) {
|
||||||
o.numWords = DefaultOptions.numWords;
|
o.numWords = DefaultOptions.numWords;
|
||||||
@@ -192,65 +190,25 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr
|
|||||||
async enforcePasswordGeneratorPoliciesOnOptions(
|
async enforcePasswordGeneratorPoliciesOnOptions(
|
||||||
options: PasswordGeneratorOptions,
|
options: PasswordGeneratorOptions,
|
||||||
): Promise<[PasswordGeneratorOptions, PasswordGeneratorPolicyOptions]> {
|
): Promise<[PasswordGeneratorOptions, PasswordGeneratorPolicyOptions]> {
|
||||||
let enforcedPolicyOptions = await this.getPasswordGeneratorPolicyOptions();
|
let policy = await this.getPasswordGeneratorPolicyOptions();
|
||||||
if (enforcedPolicyOptions != null) {
|
policy = policy ?? new PasswordGeneratorPolicyOptions();
|
||||||
if (options.length < enforcedPolicyOptions.minLength) {
|
|
||||||
options.length = enforcedPolicyOptions.minLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (enforcedPolicyOptions.useUppercase) {
|
// Force default type if password/passphrase selected via policy
|
||||||
options.uppercase = true;
|
if (policy.defaultType === "password" || policy.defaultType === "passphrase") {
|
||||||
}
|
options.type = policy.defaultType;
|
||||||
|
|
||||||
if (enforcedPolicyOptions.useLowercase) {
|
|
||||||
options.lowercase = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (enforcedPolicyOptions.useNumbers) {
|
|
||||||
options.number = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.minNumber < enforcedPolicyOptions.numberCount) {
|
|
||||||
options.minNumber = enforcedPolicyOptions.numberCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (enforcedPolicyOptions.useSpecial) {
|
|
||||||
options.special = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.minSpecial < enforcedPolicyOptions.specialCount) {
|
|
||||||
options.minSpecial = enforcedPolicyOptions.specialCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Must normalize these fields because the receiving call expects all options to pass the current rules
|
|
||||||
if (options.minSpecial + options.minNumber > options.length) {
|
|
||||||
options.minSpecial = options.length - options.minNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.numWords < enforcedPolicyOptions.minNumberWords) {
|
|
||||||
options.numWords = enforcedPolicyOptions.minNumberWords;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (enforcedPolicyOptions.capitalize) {
|
|
||||||
options.capitalize = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (enforcedPolicyOptions.includeNumber) {
|
|
||||||
options.includeNumber = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Force default type if password/passphrase selected via policy
|
|
||||||
if (
|
|
||||||
enforcedPolicyOptions.defaultType === "password" ||
|
|
||||||
enforcedPolicyOptions.defaultType === "passphrase"
|
|
||||||
) {
|
|
||||||
options.type = enforcedPolicyOptions.defaultType;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// UI layer expects an instantiated object to prevent more explicit null checks
|
|
||||||
enforcedPolicyOptions = new PasswordGeneratorPolicyOptions();
|
|
||||||
}
|
}
|
||||||
return [options, enforcedPolicyOptions];
|
|
||||||
|
const evaluator = options.type
|
||||||
|
? new PasswordGeneratorOptionsEvaluator(policy)
|
||||||
|
: new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
|
|
||||||
|
// Ensure the options to pass the current rules
|
||||||
|
const withPolicy = evaluator.applyPolicy(options);
|
||||||
|
const sanitized = evaluator.sanitize(withPolicy);
|
||||||
|
|
||||||
|
// callers assume this function updates the options parameter
|
||||||
|
const result = Object.assign(options, sanitized);
|
||||||
|
return [result, policy];
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPasswordGeneratorPolicyOptions(): Promise<PasswordGeneratorPolicyOptions> {
|
async getPasswordGeneratorPolicyOptions(): Promise<PasswordGeneratorPolicyOptions> {
|
||||||
@@ -389,62 +347,17 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr
|
|||||||
options: PasswordGeneratorOptions,
|
options: PasswordGeneratorOptions,
|
||||||
enforcedPolicyOptions: PasswordGeneratorPolicyOptions,
|
enforcedPolicyOptions: PasswordGeneratorPolicyOptions,
|
||||||
) {
|
) {
|
||||||
options.minLowercase = 0;
|
const evaluator = options.type
|
||||||
options.minUppercase = 0;
|
? new PasswordGeneratorOptionsEvaluator(enforcedPolicyOptions)
|
||||||
|
: new PassphraseGeneratorOptionsEvaluator(enforcedPolicyOptions);
|
||||||
|
|
||||||
if (!options.length || options.length < 5) {
|
const evaluatedOptions = evaluator.applyPolicy(options);
|
||||||
options.length = 5;
|
const santizedOptions = evaluator.sanitize(evaluatedOptions);
|
||||||
} else if (options.length > 128) {
|
|
||||||
options.length = 128;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.length < enforcedPolicyOptions.minLength) {
|
// callers assume this function updates the options parameter
|
||||||
options.length = enforcedPolicyOptions.minLength;
|
Object.assign(options, santizedOptions);
|
||||||
}
|
|
||||||
|
|
||||||
if (!options.minNumber) {
|
return options;
|
||||||
options.minNumber = 0;
|
|
||||||
} else if (options.minNumber > options.length) {
|
|
||||||
options.minNumber = options.length;
|
|
||||||
} else if (options.minNumber > 9) {
|
|
||||||
options.minNumber = 9;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.minNumber < enforcedPolicyOptions.numberCount) {
|
|
||||||
options.minNumber = enforcedPolicyOptions.numberCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!options.minSpecial) {
|
|
||||||
options.minSpecial = 0;
|
|
||||||
} else if (options.minSpecial > options.length) {
|
|
||||||
options.minSpecial = options.length;
|
|
||||||
} else if (options.minSpecial > 9) {
|
|
||||||
options.minSpecial = 9;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.minSpecial < enforcedPolicyOptions.specialCount) {
|
|
||||||
options.minSpecial = enforcedPolicyOptions.specialCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.minSpecial + options.minNumber > options.length) {
|
|
||||||
options.minSpecial = options.length - options.minNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.numWords == null || options.length < 3) {
|
|
||||||
options.numWords = 3;
|
|
||||||
} else if (options.numWords > 20) {
|
|
||||||
options.numWords = 20;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.numWords < enforcedPolicyOptions.minNumberWords) {
|
|
||||||
options.numWords = enforcedPolicyOptions.minNumberWords;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.wordSeparator != null && options.wordSeparator.length > 1) {
|
|
||||||
options.wordSeparator = options.wordSeparator[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
this.sanitizePasswordLength(options, false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private capitalize(str: string) {
|
private capitalize(str: string) {
|
||||||
@@ -505,54 +418,4 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr
|
|||||||
[array[i], array[j]] = [array[j], array[i]];
|
[array[i], array[j]] = [array[j], array[i]];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private sanitizePasswordLength(options: any, forGeneration: boolean) {
|
|
||||||
let minUppercaseCalc = 0;
|
|
||||||
let minLowercaseCalc = 0;
|
|
||||||
let minNumberCalc: number = options.minNumber;
|
|
||||||
let minSpecialCalc: number = options.minSpecial;
|
|
||||||
|
|
||||||
if (options.uppercase && options.minUppercase <= 0) {
|
|
||||||
minUppercaseCalc = 1;
|
|
||||||
} else if (!options.uppercase) {
|
|
||||||
minUppercaseCalc = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.lowercase && options.minLowercase <= 0) {
|
|
||||||
minLowercaseCalc = 1;
|
|
||||||
} else if (!options.lowercase) {
|
|
||||||
minLowercaseCalc = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.number && options.minNumber <= 0) {
|
|
||||||
minNumberCalc = 1;
|
|
||||||
} else if (!options.number) {
|
|
||||||
minNumberCalc = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.special && options.minSpecial <= 0) {
|
|
||||||
minSpecialCalc = 1;
|
|
||||||
} else if (!options.special) {
|
|
||||||
minSpecialCalc = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This should never happen but is a final safety net
|
|
||||||
if (!options.length || options.length < 1) {
|
|
||||||
options.length = 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
const minLength: number = minUppercaseCalc + minLowercaseCalc + minNumberCalc + minSpecialCalc;
|
|
||||||
// Normalize and Generation both require this modification
|
|
||||||
if (options.length < minLength) {
|
|
||||||
options.length = minLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply other changes if the options object passed in is for generation
|
|
||||||
if (forGeneration) {
|
|
||||||
options.minUppercase = minUppercaseCalc;
|
|
||||||
options.minLowercase = minLowercaseCalc;
|
|
||||||
options.minNumber = minNumberCalc;
|
|
||||||
options.minSpecial = minSpecialCalc;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,703 @@
|
|||||||
|
import { PasswordGeneratorPolicyOptions } from "../../../admin-console/models/domain/password-generator-policy-options";
|
||||||
|
|
||||||
|
import { PasswordGenerationOptions } from "./password-generator-options";
|
||||||
|
import {
|
||||||
|
DefaultBoundaries,
|
||||||
|
PasswordGeneratorOptionsEvaluator,
|
||||||
|
} from "./password-generator-options-evaluator";
|
||||||
|
|
||||||
|
describe("Password generator options builder", () => {
|
||||||
|
const defaultOptions = Object.freeze({ minLength: 0 });
|
||||||
|
|
||||||
|
describe("constructor()", () => {
|
||||||
|
it("should set the policy object to a copy of the input policy", () => {
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
policy.minLength = 10; // arbitrary change for deep equality check
|
||||||
|
|
||||||
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
|
|
||||||
|
expect(builder.policy).toEqual(policy);
|
||||||
|
expect(builder.policy).not.toBe(policy);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set default boundaries when a default policy is used", () => {
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
|
||||||
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
|
|
||||||
|
expect(builder.length).toEqual(DefaultBoundaries.length);
|
||||||
|
expect(builder.minDigits).toEqual(DefaultBoundaries.minDigits);
|
||||||
|
expect(builder.minSpecialCharacters).toEqual(DefaultBoundaries.minSpecialCharacters);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([1, 2, 3, 4])(
|
||||||
|
"should use the default length boundaries when they are greater than `policy.minLength` (= %i)",
|
||||||
|
(minLength) => {
|
||||||
|
expect(minLength).toBeLessThan(DefaultBoundaries.length.min);
|
||||||
|
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
policy.minLength = minLength;
|
||||||
|
|
||||||
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
|
|
||||||
|
expect(builder.length).toEqual(DefaultBoundaries.length);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each([8, 20, 100])(
|
||||||
|
"should use `policy.minLength` (= %i) when it is greater than the default minimum length",
|
||||||
|
(expectedLength) => {
|
||||||
|
expect(expectedLength).toBeGreaterThan(DefaultBoundaries.length.min);
|
||||||
|
expect(expectedLength).toBeLessThanOrEqual(DefaultBoundaries.length.max);
|
||||||
|
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
policy.minLength = expectedLength;
|
||||||
|
|
||||||
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
|
|
||||||
|
expect(builder.length.min).toEqual(expectedLength);
|
||||||
|
expect(builder.length.max).toEqual(DefaultBoundaries.length.max);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each([150, 300, 9000])(
|
||||||
|
"should use `policy.minLength` (= %i) when it is greater than the default boundaries",
|
||||||
|
(expectedLength) => {
|
||||||
|
expect(expectedLength).toBeGreaterThan(DefaultBoundaries.length.max);
|
||||||
|
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
policy.minLength = expectedLength;
|
||||||
|
|
||||||
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
|
|
||||||
|
expect(builder.length.min).toEqual(expectedLength);
|
||||||
|
expect(builder.length.max).toEqual(expectedLength);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each([3, 5, 8, 9])(
|
||||||
|
"should use `policy.numberCount` (= %i) when it is greater than the default minimum digits",
|
||||||
|
(expectedMinDigits) => {
|
||||||
|
expect(expectedMinDigits).toBeGreaterThan(DefaultBoundaries.minDigits.min);
|
||||||
|
expect(expectedMinDigits).toBeLessThanOrEqual(DefaultBoundaries.minDigits.max);
|
||||||
|
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
policy.numberCount = expectedMinDigits;
|
||||||
|
|
||||||
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
|
|
||||||
|
expect(builder.minDigits.min).toEqual(expectedMinDigits);
|
||||||
|
expect(builder.minDigits.max).toEqual(DefaultBoundaries.minDigits.max);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each([10, 20, 400])(
|
||||||
|
"should use `policy.numberCount` (= %i) when it is greater than the default digit boundaries",
|
||||||
|
(expectedMinDigits) => {
|
||||||
|
expect(expectedMinDigits).toBeGreaterThan(DefaultBoundaries.minDigits.max);
|
||||||
|
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
policy.numberCount = expectedMinDigits;
|
||||||
|
|
||||||
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
|
|
||||||
|
expect(builder.minDigits.min).toEqual(expectedMinDigits);
|
||||||
|
expect(builder.minDigits.max).toEqual(expectedMinDigits);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each([2, 4, 6])(
|
||||||
|
"should use `policy.specialCount` (= %i) when it is greater than the default minimum special characters",
|
||||||
|
(expectedSpecialCharacters) => {
|
||||||
|
expect(expectedSpecialCharacters).toBeGreaterThan(
|
||||||
|
DefaultBoundaries.minSpecialCharacters.min,
|
||||||
|
);
|
||||||
|
expect(expectedSpecialCharacters).toBeLessThanOrEqual(
|
||||||
|
DefaultBoundaries.minSpecialCharacters.max,
|
||||||
|
);
|
||||||
|
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
policy.specialCount = expectedSpecialCharacters;
|
||||||
|
|
||||||
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
|
|
||||||
|
expect(builder.minSpecialCharacters.min).toEqual(expectedSpecialCharacters);
|
||||||
|
expect(builder.minSpecialCharacters.max).toEqual(
|
||||||
|
DefaultBoundaries.minSpecialCharacters.max,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each([10, 20, 400])(
|
||||||
|
"should use `policy.specialCount` (= %i) when it is greater than the default special characters boundaries",
|
||||||
|
(expectedSpecialCharacters) => {
|
||||||
|
expect(expectedSpecialCharacters).toBeGreaterThan(
|
||||||
|
DefaultBoundaries.minSpecialCharacters.max,
|
||||||
|
);
|
||||||
|
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
policy.specialCount = expectedSpecialCharacters;
|
||||||
|
|
||||||
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
|
|
||||||
|
expect(builder.minSpecialCharacters.min).toEqual(expectedSpecialCharacters);
|
||||||
|
expect(builder.minSpecialCharacters.max).toEqual(expectedSpecialCharacters);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[8, 6, 2],
|
||||||
|
[6, 2, 4],
|
||||||
|
[16, 8, 8],
|
||||||
|
])(
|
||||||
|
"should ensure the minimum length (= %i) is at least the sum of minimums (= %i + %i)",
|
||||||
|
(expectedLength, numberCount, specialCount) => {
|
||||||
|
expect(expectedLength).toBeGreaterThanOrEqual(DefaultBoundaries.length.min);
|
||||||
|
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
policy.numberCount = numberCount;
|
||||||
|
policy.specialCount = specialCount;
|
||||||
|
|
||||||
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
|
|
||||||
|
expect(builder.length.min).toBeGreaterThanOrEqual(expectedLength);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("applyPolicy(options)", () => {
|
||||||
|
// All tests should freeze the options to ensure they are not modified
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[false, false],
|
||||||
|
[true, true],
|
||||||
|
[false, undefined],
|
||||||
|
])(
|
||||||
|
"should set `options.uppercase` to '%s' when `policy.useUppercase` is false and `options.uppercase` is '%s'",
|
||||||
|
(expectedUppercase, uppercase) => {
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
policy.useUppercase = false;
|
||||||
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
|
const options = Object.freeze({ ...defaultOptions, uppercase });
|
||||||
|
|
||||||
|
const sanitizedOptions = builder.applyPolicy(options);
|
||||||
|
|
||||||
|
expect(sanitizedOptions.uppercase).toEqual(expectedUppercase);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each([false, true, undefined])(
|
||||||
|
"should set `options.uppercase` (= %s) to true when `policy.useUppercase` is true",
|
||||||
|
(uppercase) => {
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
policy.useUppercase = true;
|
||||||
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
|
const options = Object.freeze({ ...defaultOptions, uppercase });
|
||||||
|
|
||||||
|
const sanitizedOptions = builder.applyPolicy(options);
|
||||||
|
|
||||||
|
expect(sanitizedOptions.uppercase).toEqual(true);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[false, false],
|
||||||
|
[true, true],
|
||||||
|
[false, undefined],
|
||||||
|
])(
|
||||||
|
"should set `options.lowercase` to '%s' when `policy.useLowercase` is false and `options.lowercase` is '%s'",
|
||||||
|
(expectedLowercase, lowercase) => {
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
policy.useLowercase = false;
|
||||||
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
|
const options = Object.freeze({ ...defaultOptions, lowercase });
|
||||||
|
|
||||||
|
const sanitizedOptions = builder.applyPolicy(options);
|
||||||
|
|
||||||
|
expect(sanitizedOptions.lowercase).toEqual(expectedLowercase);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each([false, true, undefined])(
|
||||||
|
"should set `options.lowercase` (= %s) to true when `policy.useLowercase` is true",
|
||||||
|
(lowercase) => {
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
policy.useLowercase = true;
|
||||||
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
|
const options = Object.freeze({ ...defaultOptions, lowercase });
|
||||||
|
|
||||||
|
const sanitizedOptions = builder.applyPolicy(options);
|
||||||
|
|
||||||
|
expect(sanitizedOptions.lowercase).toEqual(true);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[false, false],
|
||||||
|
[true, true],
|
||||||
|
[false, undefined],
|
||||||
|
])(
|
||||||
|
"should set `options.number` to '%s' when `policy.useNumbers` is false and `options.number` is '%s'",
|
||||||
|
(expectedNumber, number) => {
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
policy.useNumbers = false;
|
||||||
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
|
const options = Object.freeze({ ...defaultOptions, number });
|
||||||
|
|
||||||
|
const sanitizedOptions = builder.applyPolicy(options);
|
||||||
|
|
||||||
|
expect(sanitizedOptions.number).toEqual(expectedNumber);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each([false, true, undefined])(
|
||||||
|
"should set `options.number` (= %s) to true when `policy.useNumbers` is true",
|
||||||
|
(number) => {
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
policy.useNumbers = true;
|
||||||
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
|
const options = Object.freeze({ ...defaultOptions, number });
|
||||||
|
|
||||||
|
const sanitizedOptions = builder.applyPolicy(options);
|
||||||
|
|
||||||
|
expect(sanitizedOptions.number).toEqual(true);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[false, false],
|
||||||
|
[true, true],
|
||||||
|
[false, undefined],
|
||||||
|
])(
|
||||||
|
"should set `options.special` to '%s' when `policy.useSpecial` is false and `options.special` is '%s'",
|
||||||
|
(expectedSpecial, special) => {
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
policy.useSpecial = false;
|
||||||
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
|
const options = Object.freeze({ ...defaultOptions, special });
|
||||||
|
|
||||||
|
const sanitizedOptions = builder.applyPolicy(options);
|
||||||
|
|
||||||
|
expect(sanitizedOptions.special).toEqual(expectedSpecial);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each([false, true, undefined])(
|
||||||
|
"should set `options.special` (= %s) to true when `policy.useSpecial` is true",
|
||||||
|
(special) => {
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
policy.useSpecial = true;
|
||||||
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
|
const options = Object.freeze({ ...defaultOptions, special });
|
||||||
|
|
||||||
|
const sanitizedOptions = builder.applyPolicy(options);
|
||||||
|
|
||||||
|
expect(sanitizedOptions.special).toEqual(true);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each([1, 2, 3, 4])(
|
||||||
|
"should set `options.length` (= %i) to the minimum it is less than the minimum length",
|
||||||
|
(length) => {
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
|
expect(length).toBeLessThan(builder.length.min);
|
||||||
|
|
||||||
|
const options = Object.freeze({ ...defaultOptions, length });
|
||||||
|
|
||||||
|
const sanitizedOptions = builder.applyPolicy(options);
|
||||||
|
|
||||||
|
expect(sanitizedOptions.length).toEqual(builder.length.min);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each([5, 10, 50, 100, 128])(
|
||||||
|
"should not change `options.length` (= %i) when it is within the boundaries",
|
||||||
|
(length) => {
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
|
expect(length).toBeGreaterThanOrEqual(builder.length.min);
|
||||||
|
expect(length).toBeLessThanOrEqual(builder.length.max);
|
||||||
|
|
||||||
|
const options = Object.freeze({ ...defaultOptions, length });
|
||||||
|
|
||||||
|
const sanitizedOptions = builder.applyPolicy(options);
|
||||||
|
|
||||||
|
expect(sanitizedOptions.length).toEqual(length);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each([129, 500, 9000])(
|
||||||
|
"should set `options.length` (= %i) to the maximum length when it is exceeded",
|
||||||
|
(length) => {
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
|
expect(length).toBeGreaterThan(builder.length.max);
|
||||||
|
|
||||||
|
const options = Object.freeze({ ...defaultOptions, length });
|
||||||
|
|
||||||
|
const sanitizedOptions = builder.applyPolicy(options);
|
||||||
|
|
||||||
|
expect(sanitizedOptions.length).toEqual(builder.length.max);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[true, 1],
|
||||||
|
[true, 3],
|
||||||
|
[true, 600],
|
||||||
|
[false, 0],
|
||||||
|
[false, -2],
|
||||||
|
[false, -600],
|
||||||
|
])(
|
||||||
|
"should set `options.number === %s` when `options.minNumber` (= %i) is set to a value greater than 0",
|
||||||
|
(expectedNumber, minNumber) => {
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
|
const options = Object.freeze({ ...defaultOptions, minNumber });
|
||||||
|
|
||||||
|
const sanitizedOptions = builder.applyPolicy(options);
|
||||||
|
|
||||||
|
expect(sanitizedOptions.number).toEqual(expectedNumber);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it("should set `options.minNumber` to the minimum value when `options.number` is true", () => {
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
|
const options = Object.freeze({ ...defaultOptions, number: true });
|
||||||
|
|
||||||
|
const sanitizedOptions = builder.applyPolicy(options);
|
||||||
|
|
||||||
|
expect(sanitizedOptions.minNumber).toEqual(builder.minDigits.min);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set `options.minNumber` to 0 when `options.number` is false", () => {
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
|
const options = Object.freeze({ ...defaultOptions, number: false });
|
||||||
|
|
||||||
|
const sanitizedOptions = builder.applyPolicy(options);
|
||||||
|
|
||||||
|
expect(sanitizedOptions.minNumber).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([1, 2, 3, 4])(
|
||||||
|
"should set `options.minNumber` (= %i) to the minimum it is less than the minimum number",
|
||||||
|
(minNumber) => {
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
policy.numberCount = 5; // arbitrary value greater than minNumber
|
||||||
|
expect(minNumber).toBeLessThan(policy.numberCount);
|
||||||
|
|
||||||
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
|
const options = Object.freeze({ ...defaultOptions, minNumber });
|
||||||
|
|
||||||
|
const sanitizedOptions = builder.applyPolicy(options);
|
||||||
|
|
||||||
|
expect(sanitizedOptions.minNumber).toEqual(builder.minDigits.min);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each([1, 3, 5, 7, 9])(
|
||||||
|
"should not change `options.minNumber` (= %i) when it is within the boundaries",
|
||||||
|
(minNumber) => {
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
|
expect(minNumber).toBeGreaterThanOrEqual(builder.minDigits.min);
|
||||||
|
expect(minNumber).toBeLessThanOrEqual(builder.minDigits.max);
|
||||||
|
|
||||||
|
const options = Object.freeze({ ...defaultOptions, minNumber });
|
||||||
|
|
||||||
|
const sanitizedOptions = builder.applyPolicy(options);
|
||||||
|
|
||||||
|
expect(sanitizedOptions.minNumber).toEqual(minNumber);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each([10, 20, 400])(
|
||||||
|
"should set `options.minNumber` (= %i) to the maximum digit boundary when it is exceeded",
|
||||||
|
(minNumber) => {
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
|
expect(minNumber).toBeGreaterThan(builder.minDigits.max);
|
||||||
|
|
||||||
|
const options = Object.freeze({ ...defaultOptions, minNumber });
|
||||||
|
|
||||||
|
const sanitizedOptions = builder.applyPolicy(options);
|
||||||
|
|
||||||
|
expect(sanitizedOptions.minNumber).toEqual(builder.minDigits.max);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[true, 1],
|
||||||
|
[true, 3],
|
||||||
|
[true, 600],
|
||||||
|
[false, 0],
|
||||||
|
[false, -2],
|
||||||
|
[false, -600],
|
||||||
|
])(
|
||||||
|
"should set `options.special === %s` when `options.minSpecial` (= %i) is set to a value greater than 0",
|
||||||
|
(expectedSpecial, minSpecial) => {
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
|
const options = Object.freeze({ ...defaultOptions, minSpecial });
|
||||||
|
|
||||||
|
const sanitizedOptions = builder.applyPolicy(options);
|
||||||
|
|
||||||
|
expect(sanitizedOptions.special).toEqual(expectedSpecial);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it("should set `options.minSpecial` to the minimum value when `options.special` is true", () => {
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
|
const options = Object.freeze({ ...defaultOptions, special: true });
|
||||||
|
|
||||||
|
const sanitizedOptions = builder.applyPolicy(options);
|
||||||
|
|
||||||
|
expect(sanitizedOptions.minSpecial).toEqual(builder.minDigits.min);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set `options.minSpecial` to 0 when `options.special` is false", () => {
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
|
const options = Object.freeze({ ...defaultOptions, special: false });
|
||||||
|
|
||||||
|
const sanitizedOptions = builder.applyPolicy(options);
|
||||||
|
|
||||||
|
expect(sanitizedOptions.minSpecial).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([1, 2, 3, 4])(
|
||||||
|
"should set `options.minSpecial` (= %i) to the minimum it is less than the minimum special characters",
|
||||||
|
(minSpecial) => {
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
policy.specialCount = 5; // arbitrary value greater than minSpecial
|
||||||
|
expect(minSpecial).toBeLessThan(policy.specialCount);
|
||||||
|
|
||||||
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
|
const options = Object.freeze({ ...defaultOptions, minSpecial });
|
||||||
|
|
||||||
|
const sanitizedOptions = builder.applyPolicy(options);
|
||||||
|
|
||||||
|
expect(sanitizedOptions.minSpecial).toEqual(builder.minSpecialCharacters.min);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each([1, 3, 5, 7, 9])(
|
||||||
|
"should not change `options.minSpecial` (= %i) when it is within the boundaries",
|
||||||
|
(minSpecial) => {
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
|
expect(minSpecial).toBeGreaterThanOrEqual(builder.minSpecialCharacters.min);
|
||||||
|
expect(minSpecial).toBeLessThanOrEqual(builder.minSpecialCharacters.max);
|
||||||
|
|
||||||
|
const options = Object.freeze({ ...defaultOptions, minSpecial });
|
||||||
|
|
||||||
|
const sanitizedOptions = builder.applyPolicy(options);
|
||||||
|
|
||||||
|
expect(sanitizedOptions.minSpecial).toEqual(minSpecial);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each([10, 20, 400])(
|
||||||
|
"should set `options.minSpecial` (= %i) to the maximum special character boundary when it is exceeded",
|
||||||
|
(minSpecial) => {
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
|
expect(minSpecial).toBeGreaterThan(builder.minSpecialCharacters.max);
|
||||||
|
|
||||||
|
const options = Object.freeze({ ...defaultOptions, minSpecial });
|
||||||
|
|
||||||
|
const sanitizedOptions = builder.applyPolicy(options);
|
||||||
|
|
||||||
|
expect(sanitizedOptions.minSpecial).toEqual(builder.minSpecialCharacters.max);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it("should preserve unknown properties", () => {
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
|
const options = Object.freeze({
|
||||||
|
unknown: "property",
|
||||||
|
another: "unknown property",
|
||||||
|
}) as PasswordGenerationOptions;
|
||||||
|
|
||||||
|
const sanitizedOptions: any = builder.applyPolicy(options);
|
||||||
|
|
||||||
|
expect(sanitizedOptions.unknown).toEqual("property");
|
||||||
|
expect(sanitizedOptions.another).toEqual("unknown property");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("sanitize(options)", () => {
|
||||||
|
// All tests should freeze the options to ensure they are not modified
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[1, true],
|
||||||
|
[0, false],
|
||||||
|
])(
|
||||||
|
"should output `options.minLowercase === %i` when `options.lowercase` is %s",
|
||||||
|
(expectedMinLowercase, lowercase) => {
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
|
const options = Object.freeze({ lowercase, ...defaultOptions });
|
||||||
|
|
||||||
|
const actual = builder.sanitize(options);
|
||||||
|
|
||||||
|
expect(actual.minLowercase).toEqual(expectedMinLowercase);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[1, true],
|
||||||
|
[0, false],
|
||||||
|
])(
|
||||||
|
"should output `options.minUppercase === %i` when `options.uppercase` is %s",
|
||||||
|
(expectedMinUppercase, uppercase) => {
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
|
const options = Object.freeze({ uppercase, ...defaultOptions });
|
||||||
|
|
||||||
|
const actual = builder.sanitize(options);
|
||||||
|
|
||||||
|
expect(actual.minUppercase).toEqual(expectedMinUppercase);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[1, true],
|
||||||
|
[0, false],
|
||||||
|
])(
|
||||||
|
"should output `options.minNumber === %i` when `options.number` is %s and `options.minNumber` is not set",
|
||||||
|
(expectedMinNumber, number) => {
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
|
const options = Object.freeze({ number, ...defaultOptions });
|
||||||
|
|
||||||
|
const actual = builder.sanitize(options);
|
||||||
|
|
||||||
|
expect(actual.minNumber).toEqual(expectedMinNumber);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[true, 3],
|
||||||
|
[true, 2],
|
||||||
|
[true, 1],
|
||||||
|
[false, 0],
|
||||||
|
])(
|
||||||
|
"should output `options.number === %s` when `options.minNumber` is %i and `options.number` is not set",
|
||||||
|
(expectedNumber, minNumber) => {
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
|
const options = Object.freeze({ minNumber, ...defaultOptions });
|
||||||
|
|
||||||
|
const actual = builder.sanitize(options);
|
||||||
|
|
||||||
|
expect(actual.number).toEqual(expectedNumber);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[true, 1],
|
||||||
|
[false, 0],
|
||||||
|
])(
|
||||||
|
"should output `options.minSpecial === %i` when `options.special` is %s and `options.minSpecial` is not set",
|
||||||
|
(special, expectedMinSpecial) => {
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
|
const options = Object.freeze({ special, ...defaultOptions });
|
||||||
|
|
||||||
|
const actual = builder.sanitize(options);
|
||||||
|
|
||||||
|
expect(actual.minSpecial).toEqual(expectedMinSpecial);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[3, true],
|
||||||
|
[2, true],
|
||||||
|
[1, true],
|
||||||
|
[0, false],
|
||||||
|
])(
|
||||||
|
"should output `options.special === %s` when `options.minSpecial` is %i and `options.special` is not set",
|
||||||
|
(minSpecial, expectedSpecial) => {
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
|
const options = Object.freeze({ minSpecial, ...defaultOptions });
|
||||||
|
|
||||||
|
const actual = builder.sanitize(options);
|
||||||
|
|
||||||
|
expect(actual.special).toEqual(expectedSpecial);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[0, 0, 0, 0],
|
||||||
|
[1, 1, 0, 0],
|
||||||
|
[0, 0, 1, 1],
|
||||||
|
[1, 1, 1, 1],
|
||||||
|
])(
|
||||||
|
"should set `options.minLength` to the minimum boundary when the sum of minimums (%i + %i + %i + %i) is less than the default minimum length.",
|
||||||
|
(minLowercase, minUppercase, minNumber, minSpecial) => {
|
||||||
|
const sumOfMinimums = minLowercase + minUppercase + minNumber + minSpecial;
|
||||||
|
expect(sumOfMinimums).toBeLessThan(DefaultBoundaries.length.min);
|
||||||
|
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
|
const options = Object.freeze({
|
||||||
|
minLowercase,
|
||||||
|
minUppercase,
|
||||||
|
minNumber,
|
||||||
|
minSpecial,
|
||||||
|
...defaultOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
const actual = builder.sanitize(options);
|
||||||
|
|
||||||
|
expect(actual.minLength).toEqual(builder.length.min);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[12, 3, 3, 3, 3],
|
||||||
|
[8, 2, 2, 2, 2],
|
||||||
|
[9, 3, 3, 3, 0],
|
||||||
|
])(
|
||||||
|
"should set `options.minLength === %i` to the sum of minimums (%i + %i + %i + %i) when the sum is at least the default minimum length.",
|
||||||
|
(expectedMinLength, minLowercase, minUppercase, minNumber, minSpecial) => {
|
||||||
|
expect(expectedMinLength).toBeGreaterThanOrEqual(DefaultBoundaries.length.min);
|
||||||
|
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
|
const options = Object.freeze({
|
||||||
|
minLowercase,
|
||||||
|
minUppercase,
|
||||||
|
minNumber,
|
||||||
|
minSpecial,
|
||||||
|
...defaultOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
const actual = builder.sanitize(options);
|
||||||
|
|
||||||
|
expect(actual.minLength).toEqual(expectedMinLength);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it("should preserve unknown properties", () => {
|
||||||
|
const policy = new PasswordGeneratorPolicyOptions();
|
||||||
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
|
const options = Object.freeze({
|
||||||
|
unknown: "property",
|
||||||
|
another: "unknown property",
|
||||||
|
}) as PasswordGenerationOptions;
|
||||||
|
|
||||||
|
const sanitizedOptions: any = builder.sanitize(options);
|
||||||
|
|
||||||
|
expect(sanitizedOptions.unknown).toEqual("property");
|
||||||
|
expect(sanitizedOptions.another).toEqual("unknown property");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
import { PasswordGeneratorPolicyOptions } from "../../../admin-console/models/domain/password-generator-policy-options";
|
||||||
|
|
||||||
|
import { PasswordGenerationOptions } from "./password-generator-options";
|
||||||
|
|
||||||
|
function initializeBoundaries() {
|
||||||
|
const length = Object.freeze({
|
||||||
|
min: 5,
|
||||||
|
max: 128,
|
||||||
|
});
|
||||||
|
|
||||||
|
const minDigits = Object.freeze({
|
||||||
|
min: 0,
|
||||||
|
max: 9,
|
||||||
|
});
|
||||||
|
|
||||||
|
const minSpecialCharacters = Object.freeze({
|
||||||
|
min: 0,
|
||||||
|
max: 9,
|
||||||
|
});
|
||||||
|
|
||||||
|
return Object.freeze({
|
||||||
|
length,
|
||||||
|
minDigits,
|
||||||
|
minSpecialCharacters,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Immutable default boundaries for password generation.
|
||||||
|
* These are used when the policy does not override a value.
|
||||||
|
*/
|
||||||
|
export const DefaultBoundaries = initializeBoundaries();
|
||||||
|
|
||||||
|
type Boundary = {
|
||||||
|
readonly min: number;
|
||||||
|
readonly max: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Enforces policy for password generation.
|
||||||
|
*/
|
||||||
|
export class PasswordGeneratorOptionsEvaluator {
|
||||||
|
// This design is not ideal, but it is a step towards a more robust password
|
||||||
|
// generator. Ideally, `sanitize` would be implemented on an options class,
|
||||||
|
// and `applyPolicy` would be implemented on a policy class, "mise en place".
|
||||||
|
//
|
||||||
|
// The current design of the password generator, unfortunately, would require
|
||||||
|
// a substantial rewrite to make this feasible. Hopefully this change can be
|
||||||
|
// applied when the password generator is ported to rust.
|
||||||
|
|
||||||
|
/** Boundaries for the password length. This is always large enough
|
||||||
|
* to accommodate the minimum number of digits and special characters.
|
||||||
|
*/
|
||||||
|
readonly length: Boundary;
|
||||||
|
|
||||||
|
/** Boundaries for the minimum number of digits allowed in the password.
|
||||||
|
*/
|
||||||
|
readonly minDigits: Boundary;
|
||||||
|
|
||||||
|
/** Boundaries for the minimum number of special characters allowed
|
||||||
|
* in the password.
|
||||||
|
*/
|
||||||
|
readonly minSpecialCharacters: Boundary;
|
||||||
|
|
||||||
|
/** Policy applied by the evaluator.
|
||||||
|
*/
|
||||||
|
readonly policy: PasswordGeneratorPolicyOptions;
|
||||||
|
|
||||||
|
/** Instantiates the evaluator.
|
||||||
|
* @param policy The policy applied by the evaluator. When this conflicts with
|
||||||
|
* the defaults, the policy takes precedence.
|
||||||
|
*/
|
||||||
|
constructor(policy: PasswordGeneratorPolicyOptions) {
|
||||||
|
function createBoundary(value: number, defaultBoundary: Boundary): Boundary {
|
||||||
|
const boundary = {
|
||||||
|
min: Math.max(defaultBoundary.min, value),
|
||||||
|
max: Math.max(defaultBoundary.max, value),
|
||||||
|
};
|
||||||
|
|
||||||
|
return boundary;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.policy = policy.clone();
|
||||||
|
this.minDigits = createBoundary(policy.numberCount, DefaultBoundaries.minDigits);
|
||||||
|
this.minSpecialCharacters = createBoundary(
|
||||||
|
policy.specialCount,
|
||||||
|
DefaultBoundaries.minSpecialCharacters,
|
||||||
|
);
|
||||||
|
|
||||||
|
// the overall length should be at least as long as the sum of the minimums
|
||||||
|
const minConsistentLength = this.minDigits.min + this.minSpecialCharacters.min;
|
||||||
|
const minPolicyLength = policy.minLength > 0 ? policy.minLength : DefaultBoundaries.length.min;
|
||||||
|
const minLength = Math.max(minPolicyLength, minConsistentLength, DefaultBoundaries.length.min);
|
||||||
|
|
||||||
|
this.length = {
|
||||||
|
min: minLength,
|
||||||
|
max: Math.max(DefaultBoundaries.length.max, minLength),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Apply policy to a set of options.
|
||||||
|
* @param options The options to build from. These options are not altered.
|
||||||
|
* @returns A complete password generation request with policy applied.
|
||||||
|
* @remarks This method only applies policy overrides.
|
||||||
|
* Pass the result to `sanitize` to ensure consistency.
|
||||||
|
*/
|
||||||
|
applyPolicy(options: PasswordGenerationOptions): PasswordGenerationOptions {
|
||||||
|
function fitToBounds(value: number, boundaries: Boundary) {
|
||||||
|
const { min, max } = boundaries;
|
||||||
|
|
||||||
|
const withUpperBound = Math.min(value || 0, max);
|
||||||
|
const withLowerBound = Math.max(withUpperBound, min);
|
||||||
|
|
||||||
|
return withLowerBound;
|
||||||
|
}
|
||||||
|
|
||||||
|
// apply policy overrides
|
||||||
|
const uppercase = this.policy.useUppercase || options.uppercase || false;
|
||||||
|
const lowercase = this.policy.useLowercase || options.lowercase || false;
|
||||||
|
|
||||||
|
// these overrides can cascade numeric fields to boolean fields
|
||||||
|
const number = this.policy.useNumbers || options.number || options.minNumber > 0;
|
||||||
|
const special = this.policy.useSpecial || options.special || options.minSpecial > 0;
|
||||||
|
|
||||||
|
// apply boundaries; the boundaries can cascade boolean fields to numeric fields
|
||||||
|
const length = fitToBounds(options.length, this.length);
|
||||||
|
const minNumber = fitToBounds(options.minNumber, this.minDigits);
|
||||||
|
const minSpecial = fitToBounds(options.minSpecial, this.minSpecialCharacters);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...options,
|
||||||
|
length,
|
||||||
|
uppercase,
|
||||||
|
lowercase,
|
||||||
|
number,
|
||||||
|
minNumber,
|
||||||
|
special,
|
||||||
|
minSpecial,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ensures internal options consistency.
|
||||||
|
* @param options The options to cascade. These options are not altered.
|
||||||
|
* @returns A new password generation request with cascade applied.
|
||||||
|
* @remarks This method fills null and undefined values by looking at
|
||||||
|
* pairs of flags and values (e.g. `number` and `minNumber`). If the flag
|
||||||
|
* and value are inconsistent, the flag cascades to the value.
|
||||||
|
*/
|
||||||
|
sanitize(options: PasswordGenerationOptions): PasswordGenerationOptions {
|
||||||
|
function cascade(enabled: boolean, value: number): [boolean, number] {
|
||||||
|
const enabledResult = enabled ?? value > 0;
|
||||||
|
const valueResult = enabledResult ? value || 1 : 0;
|
||||||
|
|
||||||
|
return [enabledResult, valueResult];
|
||||||
|
}
|
||||||
|
|
||||||
|
const [lowercase, minLowercase] = cascade(options.lowercase, options.minLowercase);
|
||||||
|
const [uppercase, minUppercase] = cascade(options.uppercase, options.minUppercase);
|
||||||
|
const [number, minNumber] = cascade(options.number, options.minNumber);
|
||||||
|
const [special, minSpecial] = cascade(options.special, options.minSpecial);
|
||||||
|
|
||||||
|
// minimums can only increase the length
|
||||||
|
const minConsistentLength = minLowercase + minUppercase + minNumber + minSpecial;
|
||||||
|
const minLength = Math.max(minConsistentLength, this.length.min);
|
||||||
|
const length = Math.max(options.length ?? minLength, minLength);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...options,
|
||||||
|
length,
|
||||||
|
minLength,
|
||||||
|
lowercase,
|
||||||
|
minLowercase,
|
||||||
|
uppercase,
|
||||||
|
minUppercase,
|
||||||
|
number,
|
||||||
|
minNumber,
|
||||||
|
special,
|
||||||
|
minSpecial,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +1,105 @@
|
|||||||
export type PasswordGeneratorOptions = {
|
/** Request format for credential generation.
|
||||||
|
* This type includes all properties suitable for reactive data binding.
|
||||||
|
*/
|
||||||
|
export type PasswordGeneratorOptions = PasswordGenerationOptions &
|
||||||
|
PassphraseGenerationOptions & {
|
||||||
|
/** The algorithm to use for credential generation.
|
||||||
|
* Properties on @see PasswordGenerationOptions should be processed
|
||||||
|
* only when `type === "password"`.
|
||||||
|
* Properties on @see PassphraseGenerationOptions should be processed
|
||||||
|
* only when `type === "passphrase"`.
|
||||||
|
*/
|
||||||
|
type?: "password" | "passphrase";
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Request format for password credential generation.
|
||||||
|
* All members of this type may be `undefined` when the user is
|
||||||
|
* generating a passphrase.
|
||||||
|
*/
|
||||||
|
export type PasswordGenerationOptions = {
|
||||||
|
/** The length of the password selected by the user */
|
||||||
length?: number;
|
length?: number;
|
||||||
|
|
||||||
|
/** The minimum length of the password. This defaults to 5, and increases
|
||||||
|
* to ensure `minLength` is at least as large as the sum of the other minimums.
|
||||||
|
*/
|
||||||
|
minLength?: number;
|
||||||
|
|
||||||
|
/** `true` when ambiguous characters may be included in the output.
|
||||||
|
* `false` when ambiguous characters should not be included in the output.
|
||||||
|
*/
|
||||||
ambiguous?: boolean;
|
ambiguous?: boolean;
|
||||||
|
|
||||||
|
/** `true` when uppercase ASCII characters should be included in the output
|
||||||
|
* This value defaults to `false.
|
||||||
|
*/
|
||||||
uppercase?: boolean;
|
uppercase?: boolean;
|
||||||
|
|
||||||
|
/** The minimum number of uppercase characters to include in the output.
|
||||||
|
* The value is ignored when `uppercase` is `false`.
|
||||||
|
* The value defaults to 1 when `uppercase` is `true`.
|
||||||
|
*/
|
||||||
minUppercase?: number;
|
minUppercase?: number;
|
||||||
|
|
||||||
|
/** `true` when lowercase ASCII characters should be included in the output.
|
||||||
|
* This value defaults to `false`.
|
||||||
|
*/
|
||||||
lowercase?: boolean;
|
lowercase?: boolean;
|
||||||
|
|
||||||
|
/** The minimum number of lowercase characters to include in the output.
|
||||||
|
* The value defaults to 1 when `lowercase` is `true`.
|
||||||
|
* The value defaults to 0 when `lowercase` is `false`.
|
||||||
|
*/
|
||||||
minLowercase?: number;
|
minLowercase?: number;
|
||||||
|
|
||||||
|
/** Whether or not to include ASCII digits in the output
|
||||||
|
* This value defaults to `true` when `minNumber` is at least 1.
|
||||||
|
* This value defaults to `false` when `minNumber` is less than 1.
|
||||||
|
*/
|
||||||
number?: boolean;
|
number?: boolean;
|
||||||
|
|
||||||
|
/** The minimum number of digits to include in the output.
|
||||||
|
* The value defaults to 1 when `number` is `true`.
|
||||||
|
* The value defaults to 0 when `number` is `false`.
|
||||||
|
*/
|
||||||
minNumber?: number;
|
minNumber?: number;
|
||||||
|
|
||||||
|
/** Whether or not to include special characters in the output.
|
||||||
|
* This value defaults to `true` when `minSpecial` is at least 1.
|
||||||
|
* This value defaults to `false` when `minSpecial` is less than 1.
|
||||||
|
*/
|
||||||
special?: boolean;
|
special?: boolean;
|
||||||
|
|
||||||
|
/** The minimum number of special characters to include in the output.
|
||||||
|
* This value defaults to 1 when `special` is `true`.
|
||||||
|
* This value defaults to 0 when `special` is `false`.
|
||||||
|
*/
|
||||||
minSpecial?: number;
|
minSpecial?: number;
|
||||||
numWords?: number;
|
};
|
||||||
wordSeparator?: string;
|
|
||||||
capitalize?: boolean;
|
/** Request format for passphrase credential generation.
|
||||||
includeNumber?: boolean;
|
* The members of this type may be `undefined` when the user is
|
||||||
type?: "password" | "passphrase";
|
* generating a password.
|
||||||
|
*/
|
||||||
|
export type PassphraseGenerationOptions = {
|
||||||
|
/** The number of words to include in the passphrase.
|
||||||
|
* This value defaults to 4.
|
||||||
|
*/
|
||||||
|
numWords?: number;
|
||||||
|
|
||||||
|
/** The ASCII separator character to use between words in the passphrase.
|
||||||
|
* This value defaults to a dash.
|
||||||
|
* If multiple characters appear in the string, only the first character is used.
|
||||||
|
*/
|
||||||
|
wordSeparator?: string;
|
||||||
|
|
||||||
|
/** `true` when the first character of every word should be capitalized.
|
||||||
|
* This value defaults to `false`.
|
||||||
|
*/
|
||||||
|
capitalize?: boolean;
|
||||||
|
|
||||||
|
/** `true` when a number should be included in the passphrase.
|
||||||
|
* This value defaults to `false`.
|
||||||
|
*/
|
||||||
|
includeNumber?: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
82
package-lock.json
generated
82
package-lock.json
generated
@@ -37,7 +37,7 @@
|
|||||||
"bufferutil": "4.0.8",
|
"bufferutil": "4.0.8",
|
||||||
"chalk": "4.1.2",
|
"chalk": "4.1.2",
|
||||||
"commander": "7.2.0",
|
"commander": "7.2.0",
|
||||||
"core-js": "3.32.0",
|
"core-js": "3.34.0",
|
||||||
"duo_web_sdk": "github:duosecurity/duo_web_sdk",
|
"duo_web_sdk": "github:duosecurity/duo_web_sdk",
|
||||||
"form-data": "4.0.0",
|
"form-data": "4.0.0",
|
||||||
"https-proxy-agent": "5.0.1",
|
"https-proxy-agent": "5.0.1",
|
||||||
@@ -155,7 +155,7 @@
|
|||||||
"mini-css-extract-plugin": "2.7.6",
|
"mini-css-extract-plugin": "2.7.6",
|
||||||
"node-ipc": "9.2.1",
|
"node-ipc": "9.2.1",
|
||||||
"pkg": "5.8.1",
|
"pkg": "5.8.1",
|
||||||
"postcss": "8.4.31",
|
"postcss": "8.4.32",
|
||||||
"postcss-loader": "7.3.3",
|
"postcss-loader": "7.3.3",
|
||||||
"prettier": "3.1.1",
|
"prettier": "3.1.1",
|
||||||
"prettier-plugin-tailwindcss": "0.5.9",
|
"prettier-plugin-tailwindcss": "0.5.9",
|
||||||
@@ -165,7 +165,7 @@
|
|||||||
"regedit": "^3.0.3",
|
"regedit": "^3.0.3",
|
||||||
"remark-gfm": "3.0.1",
|
"remark-gfm": "3.0.1",
|
||||||
"rimraf": "5.0.5",
|
"rimraf": "5.0.5",
|
||||||
"sass": "1.65.1",
|
"sass": "1.69.5",
|
||||||
"sass-loader": "13.3.2",
|
"sass-loader": "13.3.2",
|
||||||
"storybook": "7.3.0",
|
"storybook": "7.3.0",
|
||||||
"style-loader": "3.3.3",
|
"style-loader": "3.3.3",
|
||||||
@@ -175,7 +175,7 @@
|
|||||||
"tsconfig-paths-webpack-plugin": "4.1.0",
|
"tsconfig-paths-webpack-plugin": "4.1.0",
|
||||||
"type-fest": "2.19.0",
|
"type-fest": "2.19.0",
|
||||||
"typescript": "4.9.5",
|
"typescript": "4.9.5",
|
||||||
"url": "0.11.1",
|
"url": "0.11.3",
|
||||||
"util": "0.12.5",
|
"util": "0.12.5",
|
||||||
"wait-on": "7.2.0",
|
"wait-on": "7.2.0",
|
||||||
"webpack": "5.89.0",
|
"webpack": "5.89.0",
|
||||||
@@ -893,6 +893,34 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@angular-devkit/build-angular/node_modules/postcss": {
|
||||||
|
"version": "8.4.31",
|
||||||
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
||||||
|
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/postcss/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "tidelift",
|
||||||
|
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ai"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"nanoid": "^3.3.6",
|
||||||
|
"picocolors": "^1.0.0",
|
||||||
|
"source-map-js": "^1.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^10 || ^12 || >=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@angular-devkit/build-angular/node_modules/postcss-loader": {
|
"node_modules/@angular-devkit/build-angular/node_modules/postcss-loader": {
|
||||||
"version": "7.0.2",
|
"version": "7.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.0.2.tgz",
|
||||||
@@ -18880,9 +18908,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/core-js": {
|
"node_modules/core-js": {
|
||||||
"version": "3.32.0",
|
"version": "3.34.0",
|
||||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.32.0.tgz",
|
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.34.0.tgz",
|
||||||
"integrity": "sha512-rd4rYZNlF3WuoYuRIDEmbR/ga9CeuWX9U05umAvgrrZoHY4Z++cp/xwPQMvUpBB4Ag6J8KfD80G0zwCyaSxDww==",
|
"integrity": "sha512-aDdvlDder8QmY91H88GzNi9EtQi2TjvQhpCX6B1v/dAZHU1AuLgHvRh54RiOerpEhEW46Tkf+vgAViB/CWC0ag==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -33230,9 +33258,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.4.31",
|
"version": "8.4.32",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.32.tgz",
|
||||||
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
|
"integrity": "sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -33249,7 +33277,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.6",
|
"nanoid": "^3.3.7",
|
||||||
"picocolors": "^1.0.0",
|
"picocolors": "^1.0.0",
|
||||||
"source-map-js": "^1.0.2"
|
"source-map-js": "^1.0.2"
|
||||||
},
|
},
|
||||||
@@ -33486,6 +33514,24 @@
|
|||||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/postcss/node_modules/nanoid": {
|
||||||
|
"version": "3.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
|
||||||
|
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ai"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"bin": {
|
||||||
|
"nanoid": "bin/nanoid.cjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/prebuild-install": {
|
"node_modules/prebuild-install": {
|
||||||
"version": "7.1.1",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz",
|
||||||
@@ -35644,9 +35690,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/sass": {
|
"node_modules/sass": {
|
||||||
"version": "1.65.1",
|
"version": "1.69.5",
|
||||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.65.1.tgz",
|
"resolved": "https://registry.npmjs.org/sass/-/sass-1.69.5.tgz",
|
||||||
"integrity": "sha512-9DINwtHmA41SEd36eVPQ9BJKpn7eKDQmUHmpI0y5Zv2Rcorrh0zS+cFrt050hdNbmmCNKTW3hV5mWfuegNRsEA==",
|
"integrity": "sha512-qg2+UCJibLr2LCVOt3OlPhr/dqVHWOa9XtZf2OjbLs/T4VPSJ00udtgJxH3neXZm+QqX8B+3cU7RaLqp1iVfcQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chokidar": ">=3.0.0 <4.0.0",
|
"chokidar": ">=3.0.0 <4.0.0",
|
||||||
@@ -39535,13 +39581,13 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/url": {
|
"node_modules/url": {
|
||||||
"version": "0.11.1",
|
"version": "0.11.3",
|
||||||
"resolved": "https://registry.npmjs.org/url/-/url-0.11.1.tgz",
|
"resolved": "https://registry.npmjs.org/url/-/url-0.11.3.tgz",
|
||||||
"integrity": "sha512-rWS3H04/+mzzJkv0eZ7vEDGiQbgquI1fGfOad6zKvgYQi1SzMmhl7c/DdRGxhaWrVH6z0qWITo8rpnxK/RfEhA==",
|
"integrity": "sha512-6hxOLGfZASQK/cijlZnZJTq8OXAkt/3YGfQX45vvMYXpZoo8NdWZcY73K108Jf759lS1Bv/8wXnHDTSz17dSRw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"punycode": "^1.4.1",
|
"punycode": "^1.4.1",
|
||||||
"qs": "^6.11.0"
|
"qs": "^6.11.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/url-parse": {
|
"node_modules/url-parse": {
|
||||||
|
|||||||
@@ -118,7 +118,7 @@
|
|||||||
"mini-css-extract-plugin": "2.7.6",
|
"mini-css-extract-plugin": "2.7.6",
|
||||||
"node-ipc": "9.2.1",
|
"node-ipc": "9.2.1",
|
||||||
"pkg": "5.8.1",
|
"pkg": "5.8.1",
|
||||||
"postcss": "8.4.31",
|
"postcss": "8.4.32",
|
||||||
"postcss-loader": "7.3.3",
|
"postcss-loader": "7.3.3",
|
||||||
"prettier": "3.1.1",
|
"prettier": "3.1.1",
|
||||||
"prettier-plugin-tailwindcss": "0.5.9",
|
"prettier-plugin-tailwindcss": "0.5.9",
|
||||||
@@ -128,7 +128,7 @@
|
|||||||
"regedit": "^3.0.3",
|
"regedit": "^3.0.3",
|
||||||
"remark-gfm": "3.0.1",
|
"remark-gfm": "3.0.1",
|
||||||
"rimraf": "5.0.5",
|
"rimraf": "5.0.5",
|
||||||
"sass": "1.65.1",
|
"sass": "1.69.5",
|
||||||
"sass-loader": "13.3.2",
|
"sass-loader": "13.3.2",
|
||||||
"storybook": "7.3.0",
|
"storybook": "7.3.0",
|
||||||
"style-loader": "3.3.3",
|
"style-loader": "3.3.3",
|
||||||
@@ -138,7 +138,7 @@
|
|||||||
"tsconfig-paths-webpack-plugin": "4.1.0",
|
"tsconfig-paths-webpack-plugin": "4.1.0",
|
||||||
"type-fest": "2.19.0",
|
"type-fest": "2.19.0",
|
||||||
"typescript": "4.9.5",
|
"typescript": "4.9.5",
|
||||||
"url": "0.11.1",
|
"url": "0.11.3",
|
||||||
"util": "0.12.5",
|
"util": "0.12.5",
|
||||||
"wait-on": "7.2.0",
|
"wait-on": "7.2.0",
|
||||||
"webpack": "5.89.0",
|
"webpack": "5.89.0",
|
||||||
@@ -169,7 +169,7 @@
|
|||||||
"bufferutil": "4.0.8",
|
"bufferutil": "4.0.8",
|
||||||
"chalk": "4.1.2",
|
"chalk": "4.1.2",
|
||||||
"commander": "7.2.0",
|
"commander": "7.2.0",
|
||||||
"core-js": "3.32.0",
|
"core-js": "3.34.0",
|
||||||
"duo_web_sdk": "github:duosecurity/duo_web_sdk",
|
"duo_web_sdk": "github:duosecurity/duo_web_sdk",
|
||||||
"form-data": "4.0.0",
|
"form-data": "4.0.0",
|
||||||
"https-proxy-agent": "5.0.1",
|
"https-proxy-agent": "5.0.1",
|
||||||
|
|||||||
Reference in New Issue
Block a user