mirror of
https://github.com/bitwarden/directory-connector
synced 2025-12-05 23:53:21 +00:00
Compare commits
20 Commits
ac/addison
...
v2024.9.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d355812e0 | ||
|
|
c973d5fea0 | ||
|
|
01a3f68480 | ||
|
|
d31f14cfe7 | ||
|
|
401daa0187 | ||
|
|
1951b9507d | ||
|
|
eae9cac931 | ||
|
|
21cecc3c0a | ||
|
|
21638f3fdc | ||
|
|
f47806ddd2 | ||
|
|
c304650a6a | ||
|
|
d01522bfc4 | ||
|
|
14314a3553 | ||
|
|
ffac82e865 | ||
|
|
decada8745 | ||
|
|
3a639bb8f2 | ||
|
|
a2b5dac108 | ||
|
|
6e76d8fcbd | ||
|
|
63b06f6950 | ||
|
|
f730aeba23 |
7
.github/CODEOWNERS
vendored
7
.github/CODEOWNERS
vendored
@@ -6,10 +6,3 @@
|
||||
|
||||
# Default file owners.
|
||||
* @bitwarden/team-admin-console-dev
|
||||
|
||||
# DevOps for Actions and other workflow changes.
|
||||
.github/workflows @bitwarden/dept-devops
|
||||
.github/secrets @bitwarden/dept-devops
|
||||
|
||||
# Multiple Owners
|
||||
**/package.json
|
||||
47
.github/PULL_REQUEST_TEMPLATE.md
vendored
47
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,33 +1,34 @@
|
||||
## Type of change
|
||||
## 🎟️ Tracking
|
||||
|
||||
- [ ] Bug fix
|
||||
- [ ] New feature development
|
||||
- [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc)
|
||||
- [ ] Build/deploy pipeline (DevOps)
|
||||
- [ ] Other
|
||||
<!-- Paste the link to the Jira or GitHub issue or otherwise describe / point to where this change is coming from. -->
|
||||
|
||||
## Objective
|
||||
## 📔 Objective
|
||||
|
||||
<!--Describe what the purpose of this PR is. For example: what bug you're fixing or what new feature you're adding-->
|
||||
<!-- Describe what the purpose of this PR is, for example what bug you're fixing or new feature you're adding. -->
|
||||
|
||||
## Code changes
|
||||
## 📸 Screenshots
|
||||
|
||||
<!--Explain the changes you've made to each file or major component. This should help the reviewer understand your changes-->
|
||||
<!--Also refer to any related changes or PRs in other repositories-->
|
||||
<!-- Required for any UI changes; delete if not applicable. Use fixed width images for better display. -->
|
||||
|
||||
- **file.ext:** Description of what was changed and why
|
||||
## ⏰ Reminders before review
|
||||
|
||||
## Screenshots
|
||||
- Contributor guidelines followed
|
||||
- All formatters and local linters executed and passed
|
||||
- Written new unit and / or integration tests where applicable
|
||||
- Used internationalization (i18n) for all UI strings
|
||||
- CI builds passed
|
||||
- Communicated to DevOps any deployment requirements
|
||||
- Updated any necessary documentation (Confluence, contributing docs) or informed the documentation team
|
||||
|
||||
<!--Required for any UI changes. Delete if not applicable-->
|
||||
## 🦮 Reviewer guidelines
|
||||
|
||||
## Testing requirements
|
||||
<!-- Suggested interactions but feel free to use (or not) as you desire! -->
|
||||
|
||||
<!--What functionality requires testing by QA? This includes testing new behavior and regression testing-->
|
||||
|
||||
## Before you submit
|
||||
|
||||
- [ ] I have checked for **linting** errors (`npm run lint`) (required)
|
||||
- [ ] I have added **unit tests** where it makes sense to do so (encouraged but not required)
|
||||
- [ ] This change requires a **documentation update** (notify the documentation team)
|
||||
- [ ] This change has particular **deployment requirements** (notify the DevOps team)
|
||||
- 👍 (`:+1:`) or similar for great changes
|
||||
- 📝 (`:memo:`) or ℹ️ (`:information_source:`) for notes or general info
|
||||
- ❓ (`:question:`) for questions
|
||||
- 🤔 (`:thinking:`) or 💭 (`:thought_balloon:`) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion
|
||||
- 🎨 (`:art:`) for suggestions / improvements
|
||||
- ❌ (`:x:`) or ⚠️ (`:warning:`) for more significant problems or concerns needing attention
|
||||
- 🌱 (`:seedling:`) or ♻️ (`:recycle:`) for future improvements or indications of technical debt
|
||||
- ⛏ (`:pick:`) for minor or nitpick changes
|
||||
|
||||
25
.github/renovate.json
vendored
25
.github/renovate.json
vendored
@@ -1,31 +1,12 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:base",
|
||||
"github>bitwarden/renovate-config:pin-actions",
|
||||
":combinePatchMinorReleases",
|
||||
":dependencyDashboard",
|
||||
":maintainLockFilesWeekly",
|
||||
":pinAllExceptPeerDependencies",
|
||||
":prConcurrentLimit10",
|
||||
":rebaseStalePrs",
|
||||
":separateMajorReleases",
|
||||
"group:monorepos",
|
||||
"schedule:weekends"
|
||||
],
|
||||
"extends": ["github>bitwarden/renovate-config"],
|
||||
"enabledManagers": ["github-actions", "npm"],
|
||||
"commitMessagePrefix": "[deps]:",
|
||||
"commitMessageTopic": "{{depName}}",
|
||||
"packageRules": [
|
||||
{
|
||||
"groupName": "npm minor",
|
||||
"matchManagers": ["npm"],
|
||||
"groupName": "gh minor",
|
||||
"matchManagers": ["github-actions"],
|
||||
"matchUpdateTypes": ["minor", "patch"]
|
||||
},
|
||||
{
|
||||
"matchFileNames": ["package.json"],
|
||||
"description": "Admin Console owns general dependencies",
|
||||
"reviewers": ["team:team-admin-console-dev"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
16
.github/workflows/build.yml
vendored
16
.github/workflows/build.yml
vendored
@@ -6,8 +6,6 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
- "rc"
|
||||
- "hotfix-rc"
|
||||
workflow_dispatch: {}
|
||||
|
||||
jobs:
|
||||
@@ -578,12 +576,20 @@ jobs:
|
||||
|
||||
- name: Install Node dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Set up private auth key
|
||||
run: |
|
||||
mkdir ~/private_keys
|
||||
cat << EOF > ~/private_keys/AuthKey_UFD296548T.p8
|
||||
${{ secrets.APP_STORE_CONNECT_AUTH_KEY }}
|
||||
EOF
|
||||
|
||||
- name: Build application
|
||||
run: npm run dist:mac
|
||||
env:
|
||||
APPLE_ID_USERNAME: ${{ secrets.APPLE_ID_USERNAME }}
|
||||
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
|
||||
APP_STORE_CONNECT_TEAM_ISSUER: ${{ secrets.APP_STORE_CONNECT_TEAM_ISSUER }}
|
||||
APP_STORE_CONNECT_AUTH_KEY: UFD296548T
|
||||
APP_STORE_CONNECT_AUTH_KEY_PATH: ~/private_keys/AuthKey_UFD296548T.p8
|
||||
CSC_FOR_PULL_REQUEST: true
|
||||
|
||||
- name: Upload .zip artifact
|
||||
@@ -629,7 +635,7 @@ jobs:
|
||||
- macos-gui
|
||||
steps:
|
||||
- name: Check if any job failed
|
||||
if: (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc') && contains(needs.*.result, 'failure')
|
||||
if: github.ref == 'refs/heads/main' && contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
|
||||
- name: Login to Azure - CI subscription
|
||||
|
||||
53
.github/workflows/cleanup-rc-branch.yml
vendored
53
.github/workflows/cleanup-rc-branch.yml
vendored
@@ -1,53 +0,0 @@
|
||||
---
|
||||
name: Cleanup RC Branch
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v**
|
||||
|
||||
jobs:
|
||||
delete-rc:
|
||||
name: Delete RC Branch
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Login to Azure - CI Subscription
|
||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
|
||||
- name: Retrieve bot secrets
|
||||
id: retrieve-bot-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
keyvault: bitwarden-ci
|
||||
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
|
||||
|
||||
- name: Checkout main
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
with:
|
||||
ref: main
|
||||
token: ${{ steps.retrieve-bot-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
||||
|
||||
- name: Check if a RC branch exists
|
||||
id: branch-check
|
||||
run: |
|
||||
hotfix_rc_branch_check=$(git ls-remote --heads origin hotfix-rc | wc -l)
|
||||
rc_branch_check=$(git ls-remote --heads origin rc | wc -l)
|
||||
|
||||
if [[ "${hotfix_rc_branch_check}" -gt 0 ]]; then
|
||||
echo "hotfix-rc branch exists." | tee -a $GITHUB_STEP_SUMMARY
|
||||
echo "name=hotfix-rc" >> $GITHUB_OUTPUT
|
||||
elif [[ "${rc_branch_check}" -gt 0 ]]; then
|
||||
echo "rc branch exists." | tee -a $GITHUB_STEP_SUMMARY
|
||||
echo "name=rc" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Delete RC branch
|
||||
env:
|
||||
BRANCH_NAME: ${{ steps.branch-check.outputs.name }}
|
||||
run: |
|
||||
if ! [[ -z "$BRANCH_NAME" ]]; then
|
||||
git push --quiet origin --delete $BRANCH_NAME
|
||||
echo "Deleted $BRANCH_NAME branch." | tee -a $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
32
.github/workflows/release.yml
vendored
32
.github/workflows/release.yml
vendored
@@ -27,9 +27,9 @@ jobs:
|
||||
- name: Branch check
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||
run: |
|
||||
if [[ "$GITHUB_REF" != "refs/heads/rc" ]] && [[ "$GITHUB_REF" != "refs/heads/hotfix-rc" ]]; then
|
||||
if [[ "$GITHUB_REF" != "refs/heads/main" ]]; then
|
||||
echo "==================================="
|
||||
echo "[!] Can only release from the 'rc' or 'hotfix-rc' branches"
|
||||
echo "[!] Can only release from the 'main' branch"
|
||||
echo "==================================="
|
||||
exit 1
|
||||
fi
|
||||
@@ -47,16 +47,6 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
needs: setup
|
||||
steps:
|
||||
- name: Create GitHub deployment
|
||||
uses: chrnorm/deployment-action@55729fcebec3d284f60f5bcabbd8376437d696b1 # v2.0.7
|
||||
id: deployment
|
||||
with:
|
||||
token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
initial-status: 'in_progress'
|
||||
environment: 'production'
|
||||
description: 'Deployment ${{ needs.setup.outputs.release-version }} from branch ${{ github.ref_name }}'
|
||||
task: release
|
||||
|
||||
- name: Download all artifacts
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@main
|
||||
@@ -65,7 +55,7 @@ jobs:
|
||||
workflow_conclusion: success
|
||||
branch: ${{ github.ref_name }}
|
||||
|
||||
- name: Download all artifacts
|
||||
- name: Dry Run - Download all artifacts
|
||||
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@main
|
||||
with:
|
||||
@@ -101,19 +91,3 @@ jobs:
|
||||
body: "<insert release notes here>"
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
draft: true
|
||||
|
||||
- name: Update deployment status to Success
|
||||
if: ${{ success() }}
|
||||
uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3
|
||||
with:
|
||||
token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
state: 'success'
|
||||
deployment-id: ${{ steps.deployment.outputs.deployment_id }}
|
||||
|
||||
- name: Update deployment status to Failure
|
||||
if: ${{ failure() }}
|
||||
uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3
|
||||
with:
|
||||
token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
state: 'failure'
|
||||
deployment-id: ${{ steps.deployment.outputs.deployment_id }}
|
||||
|
||||
78
.github/workflows/scan.yml
vendored
Normal file
78
.github/workflows/scan.yml
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
name: Scan
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
pull_request_target:
|
||||
types: [opened, synchronize]
|
||||
|
||||
jobs:
|
||||
check-run:
|
||||
name: Check PR run
|
||||
uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
|
||||
|
||||
sast:
|
||||
name: SAST scan
|
||||
runs-on: ubuntu-22.04
|
||||
needs: check-run
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
security-events: write
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Scan with Checkmarx
|
||||
uses: checkmarx/ast-github-action@6c56658230f79c227a55120e9b24845d574d5225 # 2.0.31
|
||||
env:
|
||||
INCREMENTAL: "${{ contains(github.event_name, 'pull_request') && '--sast-incremental' || '' }}"
|
||||
with:
|
||||
project_name: ${{ github.repository }}
|
||||
cx_tenant: ${{ secrets.CHECKMARX_TENANT }}
|
||||
base_uri: https://ast.checkmarx.net/
|
||||
cx_client_id: ${{ secrets.CHECKMARX_CLIENT_ID }}
|
||||
cx_client_secret: ${{ secrets.CHECKMARX_SECRET }}
|
||||
additional_params: |
|
||||
--report-format sarif \
|
||||
--filter "state=TO_VERIFY;PROPOSED_NOT_EXPLOITABLE;CONFIRMED;URGENT" \
|
||||
--output-path . ${{ env.INCREMENTAL }}
|
||||
|
||||
- name: Upload Checkmarx results to GitHub
|
||||
uses: github/codeql-action/upload-sarif@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9
|
||||
with:
|
||||
sarif_file: cx_result.sarif
|
||||
|
||||
quality:
|
||||
name: Quality scan
|
||||
runs-on: ubuntu-22.04
|
||||
needs: check-run
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Scan with SonarCloud
|
||||
uses: sonarsource/sonarcloud-github-action@e44258b109568baa0df60ed515909fc6c72cba92 # v2.3.0
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
args: >
|
||||
-Dsonar.organization=${{ github.repository_owner }}
|
||||
-Dsonar.projectKey=${{ github.repository_owner }}_${{ github.event.repository.name }}
|
||||
-Dsonar.tests=.
|
||||
-Dsonar.sources=.
|
||||
-Dsonar.test.inclusions=**/*.spec.ts
|
||||
-Dsonar.exclusions=**/*.spec.ts
|
||||
72
.github/workflows/test.yml
vendored
72
.github/workflows/test.yml
vendored
@@ -1,28 +1,46 @@
|
||||
---
|
||||
name: Run tests
|
||||
name: Testing
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
- "rc"
|
||||
- "hotfix-rc-*"
|
||||
pull_request: {}
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Run tests
|
||||
check-test-secrets:
|
||||
name: Check for test secrets
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
available: ${{ steps.check-test-secrets.outputs.available }}
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
- name: Check
|
||||
id: check-test-secrets
|
||||
run: |
|
||||
if [ "${{ secrets.CODECOV_TOKEN }}" != '' ]; then
|
||||
echo "available=true" >> $GITHUB_OUTPUT;
|
||||
else
|
||||
echo "available=false" >> $GITHUB_OUTPUT;
|
||||
fi
|
||||
|
||||
testing:
|
||||
name: Run tests
|
||||
if: ${{ startsWith(github.head_ref, 'version_bump_') == false }}
|
||||
runs-on: ubuntu-22.04
|
||||
needs: check-test-secrets
|
||||
permissions:
|
||||
checks: write
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
|
||||
- name: Get Node Version
|
||||
- name: Get Node version
|
||||
id: retrieve-node-version
|
||||
run: |
|
||||
NODE_NVMRC=$(cat .nvmrc)
|
||||
@@ -36,11 +54,6 @@ jobs:
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
node-version: ${{ steps.retrieve-node-version.outputs.node_version }}
|
||||
|
||||
- name: Print environment
|
||||
run: |
|
||||
node --version
|
||||
npm --version
|
||||
|
||||
- name: Install Node dependencies
|
||||
run: npm ci
|
||||
|
||||
@@ -51,4 +64,25 @@ jobs:
|
||||
run: npm run test:types --coverage
|
||||
|
||||
- name: Run tests
|
||||
run: npm run test
|
||||
run: npm run test --coverage
|
||||
|
||||
- name: Report test results
|
||||
uses: dorny/test-reporter@31a54ee7ebcacc03a09ea97a7e5465a47b84aea5 # v1.9.1
|
||||
if: ${{ needs.check-test-secrets.outputs.available == 'true' && !cancelled() }}
|
||||
with:
|
||||
name: Test Results
|
||||
path: "junit.xml"
|
||||
reporter: jest-junit
|
||||
fail-on-error: true
|
||||
|
||||
- name: Upload coverage to codecov.io
|
||||
uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0
|
||||
if: ${{ needs.check-test-secrets.outputs.available == 'true' }}
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
- name: Upload results to codecov.io
|
||||
uses: codecov/test-results-action@1b5b448b98e58ba90d1a1a1d9fcb72ca2263be46 # v1.0.0
|
||||
if: ${{ needs.check-test-secrets.outputs.available == 'true' }}
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
47
.github/workflows/version-bump.yml
vendored
47
.github/workflows/version-bump.yml
vendored
@@ -8,10 +8,6 @@ on:
|
||||
description: "New version override (leave blank for automatic calculation, example: '2024.1.0')"
|
||||
required: false
|
||||
type: string
|
||||
cut_rc_branch:
|
||||
description: "Cut RC branch?"
|
||||
default: true
|
||||
type: boolean
|
||||
enable_slack_notification:
|
||||
description: "Enable Slack notifications for upcoming release?"
|
||||
default: false
|
||||
@@ -40,18 +36,6 @@ jobs:
|
||||
|
||||
- name: Checkout Branch
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
with:
|
||||
ref: main
|
||||
|
||||
- name: Check if RC branch exists
|
||||
if: ${{ inputs.cut_rc_branch == true }}
|
||||
run: |
|
||||
remote_rc_branch_check=$(git ls-remote --heads origin rc | wc -l)
|
||||
if [[ "${remote_rc_branch_check}" -gt 0 ]]; then
|
||||
echo "Remote RC branch exists."
|
||||
echo "Please delete current RC branch before running again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Login to Azure - CI Subscription
|
||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||
@@ -212,34 +196,3 @@ jobs:
|
||||
version: ${{ steps.set-final-version-output.outputs.version }}
|
||||
project: ${{ github.repository }}
|
||||
AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
|
||||
cut_rc:
|
||||
name: Cut RC branch
|
||||
if: ${{ inputs.cut_rc_branch == true }}
|
||||
needs: bump_version
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout Branch
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
with:
|
||||
ref: main
|
||||
|
||||
- name: Verify version has been updated
|
||||
env:
|
||||
NEW_VERSION: ${{ needs.bump_version.outputs.version }}
|
||||
run: |
|
||||
# Wait for version to change.
|
||||
while : ; do
|
||||
echo "Waiting for version to be updated..."
|
||||
git pull --force
|
||||
CURRENT_VERSION=$(cat package.json | jq -r '.version')
|
||||
|
||||
# If the versions don't match we continue the loop, otherwise we break out of the loop.
|
||||
[[ "$NEW_VERSION" != "$CURRENT_VERSION" ]] || break
|
||||
sleep 10
|
||||
done
|
||||
|
||||
- name: Cut RC branch
|
||||
run: |
|
||||
git switch --quiet --create rc
|
||||
git push --quiet --set-upstream origin rc
|
||||
|
||||
@@ -11,6 +11,14 @@ module.exports = {
|
||||
// ...angularPreset,
|
||||
preset: "jest-preset-angular",
|
||||
|
||||
reporters: ["default", "jest-junit"],
|
||||
|
||||
collectCoverage: true,
|
||||
// Ensure we collect coverage from files without tests
|
||||
collectCoverageFrom: ["src/**/*.ts"],
|
||||
coverageReporters: ["html", "lcov"],
|
||||
coverageDirectory: "coverage",
|
||||
|
||||
testEnvironment: "jsdom",
|
||||
testMatch: ["**/+(*.)+(spec).+(ts)"],
|
||||
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from "@angular/router";
|
||||
|
||||
import { KeyConnectorService } from "@/jslib/common/src/abstractions/keyConnector.service";
|
||||
import { MessagingService } from "@/jslib/common/src/abstractions/messaging.service";
|
||||
import { StateService } from "@/jslib/common/src/abstractions/state.service";
|
||||
|
||||
@Injectable()
|
||||
export class AuthGuardService {
|
||||
constructor(
|
||||
private router: Router,
|
||||
private messagingService: MessagingService,
|
||||
private keyConnectorService: KeyConnectorService,
|
||||
private stateService: StateService,
|
||||
) {}
|
||||
|
||||
async canActivate(route: ActivatedRouteSnapshot, routerState: RouterStateSnapshot) {
|
||||
const isAuthed = await this.stateService.getIsAuthenticated();
|
||||
if (!isAuthed) {
|
||||
this.messagingService.send("authBlocked");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
!routerState.url.includes("remove-password") &&
|
||||
(await this.keyConnectorService.getConvertAccountRequired())
|
||||
) {
|
||||
this.router.navigate(["/remove-password"]);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -7,25 +7,19 @@ import { BroadcasterService as BroadcasterServiceAbstraction } from "@/jslib/com
|
||||
import { CryptoService as CryptoServiceAbstraction } from "@/jslib/common/src/abstractions/crypto.service";
|
||||
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@/jslib/common/src/abstractions/cryptoFunction.service";
|
||||
import { EnvironmentService as EnvironmentServiceAbstraction } from "@/jslib/common/src/abstractions/environment.service";
|
||||
import { EventService as EventServiceAbstraction } from "@/jslib/common/src/abstractions/event.service";
|
||||
import { I18nService as I18nServiceAbstraction } from "@/jslib/common/src/abstractions/i18n.service";
|
||||
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@/jslib/common/src/abstractions/keyConnector.service";
|
||||
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
||||
import { MessagingService as MessagingServiceAbstraction } from "@/jslib/common/src/abstractions/messaging.service";
|
||||
import { NotificationsService as NotificationsServiceAbstraction } from "@/jslib/common/src/abstractions/notifications.service";
|
||||
import { OrganizationService as OrganizationServiceAbstraction } from "@/jslib/common/src/abstractions/organization.service";
|
||||
import { PasswordGenerationService as PasswordGenerationServiceAbstraction } from "@/jslib/common/src/abstractions/passwordGeneration.service";
|
||||
import { PasswordRepromptService as PasswordRepromptServiceAbstraction } from "@/jslib/common/src/abstractions/passwordReprompt.service";
|
||||
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@/jslib/common/src/abstractions/platformUtils.service";
|
||||
import { SearchService as SearchServiceAbstraction } from "@/jslib/common/src/abstractions/search.service";
|
||||
import { SettingsService as SettingsServiceAbstraction } from "@/jslib/common/src/abstractions/settings.service";
|
||||
import { PolicyService as PolicyServiceAbstraction } from "@/jslib/common/src/abstractions/policy.service";
|
||||
import { StateService as StateServiceAbstraction } from "@/jslib/common/src/abstractions/state.service";
|
||||
import { StateMigrationService as StateMigrationServiceAbstraction } from "@/jslib/common/src/abstractions/stateMigration.service";
|
||||
import { StorageService as StorageServiceAbstraction } from "@/jslib/common/src/abstractions/storage.service";
|
||||
import { SyncService as SyncServiceAbstraction } from "@/jslib/common/src/abstractions/sync.service";
|
||||
import { TokenService as TokenServiceAbstraction } from "@/jslib/common/src/abstractions/token.service";
|
||||
import { TwoFactorService as TwoFactorServiceAbstraction } from "@/jslib/common/src/abstractions/twoFactor.service";
|
||||
import { UserVerificationService as UserVerificationServiceAbstraction } from "@/jslib/common/src/abstractions/userVerification.service";
|
||||
import { StateFactory } from "@/jslib/common/src/factories/stateFactory";
|
||||
import { Account } from "@/jslib/common/src/models/domain/account";
|
||||
import { GlobalState } from "@/jslib/common/src/models/domain/globalState";
|
||||
@@ -35,26 +29,17 @@ import { AuthService } from "@/jslib/common/src/services/auth.service";
|
||||
import { ConsoleLogService } from "@/jslib/common/src/services/consoleLog.service";
|
||||
import { CryptoService } from "@/jslib/common/src/services/crypto.service";
|
||||
import { EnvironmentService } from "@/jslib/common/src/services/environment.service";
|
||||
import { EventService } from "@/jslib/common/src/services/event.service";
|
||||
import { KeyConnectorService } from "@/jslib/common/src/services/keyConnector.service";
|
||||
import { NotificationsService } from "@/jslib/common/src/services/notifications.service";
|
||||
import { OrganizationService } from "@/jslib/common/src/services/organization.service";
|
||||
import { PasswordGenerationService } from "@/jslib/common/src/services/passwordGeneration.service";
|
||||
import { SearchService } from "@/jslib/common/src/services/search.service";
|
||||
import { SettingsService } from "@/jslib/common/src/services/settings.service";
|
||||
import { PolicyService } from "@/jslib/common/src/services/policy.service";
|
||||
import { StateService } from "@/jslib/common/src/services/state.service";
|
||||
import { StateMigrationService } from "@/jslib/common/src/services/stateMigration.service";
|
||||
import { SyncService } from "@/jslib/common/src/services/sync.service";
|
||||
import { TokenService } from "@/jslib/common/src/services/token.service";
|
||||
import { TwoFactorService } from "@/jslib/common/src/services/twoFactor.service";
|
||||
import { UserVerificationService } from "@/jslib/common/src/services/userVerification.service";
|
||||
|
||||
import { AuthGuardService } from "./auth-guard.service";
|
||||
import { BroadcasterService } from "./broadcaster.service";
|
||||
import { LockGuardService } from "./lock-guard.service";
|
||||
import { ModalService } from "./modal.service";
|
||||
import { PasswordRepromptService } from "./passwordReprompt.service";
|
||||
import { UnauthGuardService } from "./unauth-guard.service";
|
||||
import { ValidationService } from "./validation.service";
|
||||
|
||||
@NgModule({
|
||||
@@ -67,9 +52,6 @@ import { ValidationService } from "./validation.service";
|
||||
deps: [I18nServiceAbstraction],
|
||||
},
|
||||
ValidationService,
|
||||
AuthGuardService,
|
||||
UnauthGuardService,
|
||||
LockGuardService,
|
||||
ModalService,
|
||||
{
|
||||
provide: AppIdServiceAbstraction,
|
||||
@@ -114,7 +96,7 @@ import { ValidationService } from "./validation.service";
|
||||
{
|
||||
provide: PasswordGenerationServiceAbstraction,
|
||||
useClass: PasswordGenerationService,
|
||||
deps: [CryptoServiceAbstraction, StateServiceAbstraction],
|
||||
deps: [CryptoServiceAbstraction, PolicyServiceAbstraction, StateServiceAbstraction],
|
||||
},
|
||||
{
|
||||
provide: ApiServiceAbstraction,
|
||||
@@ -140,46 +122,7 @@ import { ValidationService } from "./validation.service";
|
||||
AppIdServiceAbstraction,
|
||||
],
|
||||
},
|
||||
{
|
||||
provide: SyncServiceAbstraction,
|
||||
useFactory: (
|
||||
apiService: ApiServiceAbstraction,
|
||||
settingsService: SettingsServiceAbstraction,
|
||||
cryptoService: CryptoServiceAbstraction,
|
||||
messagingService: MessagingServiceAbstraction,
|
||||
logService: LogService,
|
||||
keyConnectorService: KeyConnectorServiceAbstraction,
|
||||
stateService: StateServiceAbstraction,
|
||||
organizationService: OrganizationServiceAbstraction,
|
||||
) =>
|
||||
new SyncService(
|
||||
apiService,
|
||||
settingsService,
|
||||
cryptoService,
|
||||
messagingService,
|
||||
logService,
|
||||
keyConnectorService,
|
||||
stateService,
|
||||
organizationService,
|
||||
async (expired: boolean) => messagingService.send("logout", { expired: expired }),
|
||||
),
|
||||
deps: [
|
||||
ApiServiceAbstraction,
|
||||
SettingsServiceAbstraction,
|
||||
CryptoServiceAbstraction,
|
||||
MessagingServiceAbstraction,
|
||||
LogService,
|
||||
KeyConnectorServiceAbstraction,
|
||||
StateServiceAbstraction,
|
||||
OrganizationServiceAbstraction,
|
||||
],
|
||||
},
|
||||
{ provide: BroadcasterServiceAbstraction, useClass: BroadcasterService },
|
||||
{
|
||||
provide: SettingsServiceAbstraction,
|
||||
useClass: SettingsService,
|
||||
deps: [StateServiceAbstraction],
|
||||
},
|
||||
{
|
||||
provide: StateServiceAbstraction,
|
||||
useFactory: (
|
||||
@@ -216,49 +159,9 @@ import { ValidationService } from "./validation.service";
|
||||
deps: [StorageServiceAbstraction, "SECURE_STORAGE"],
|
||||
},
|
||||
{
|
||||
provide: SearchServiceAbstraction,
|
||||
useClass: SearchService,
|
||||
deps: [LogService, I18nServiceAbstraction],
|
||||
},
|
||||
{
|
||||
provide: NotificationsServiceAbstraction,
|
||||
useFactory: (
|
||||
syncService: SyncServiceAbstraction,
|
||||
appIdService: AppIdServiceAbstraction,
|
||||
apiService: ApiServiceAbstraction,
|
||||
environmentService: EnvironmentServiceAbstraction,
|
||||
messagingService: MessagingServiceAbstraction,
|
||||
logService: LogService,
|
||||
stateService: StateServiceAbstraction,
|
||||
) =>
|
||||
new NotificationsService(
|
||||
syncService,
|
||||
appIdService,
|
||||
apiService,
|
||||
environmentService,
|
||||
async () => messagingService.send("logout", { expired: true }),
|
||||
logService,
|
||||
stateService,
|
||||
),
|
||||
deps: [
|
||||
SyncServiceAbstraction,
|
||||
AppIdServiceAbstraction,
|
||||
ApiServiceAbstraction,
|
||||
EnvironmentServiceAbstraction,
|
||||
MessagingServiceAbstraction,
|
||||
LogService,
|
||||
StateServiceAbstraction,
|
||||
],
|
||||
},
|
||||
{
|
||||
provide: EventServiceAbstraction,
|
||||
useClass: EventService,
|
||||
deps: [
|
||||
ApiServiceAbstraction,
|
||||
StateServiceAbstraction,
|
||||
LogService,
|
||||
OrganizationServiceAbstraction,
|
||||
],
|
||||
provide: PolicyServiceAbstraction,
|
||||
useClass: PolicyService,
|
||||
deps: [StateServiceAbstraction, OrganizationServiceAbstraction, ApiServiceAbstraction],
|
||||
},
|
||||
{
|
||||
provide: KeyConnectorServiceAbstraction,
|
||||
@@ -273,12 +176,6 @@ import { ValidationService } from "./validation.service";
|
||||
CryptoFunctionServiceAbstraction,
|
||||
],
|
||||
},
|
||||
{
|
||||
provide: UserVerificationServiceAbstraction,
|
||||
useClass: UserVerificationService,
|
||||
deps: [CryptoServiceAbstraction, I18nServiceAbstraction, ApiServiceAbstraction],
|
||||
},
|
||||
{ provide: PasswordRepromptServiceAbstraction, useClass: PasswordRepromptService },
|
||||
{
|
||||
provide: OrganizationServiceAbstraction,
|
||||
useClass: OrganizationService,
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
|
||||
import { StateService } from "@/jslib/common/src/abstractions/state.service";
|
||||
|
||||
@Injectable()
|
||||
export class LockGuardService {
|
||||
protected homepage = "vault";
|
||||
protected loginpage = "login";
|
||||
constructor(
|
||||
private router: Router,
|
||||
private stateService: StateService,
|
||||
) {}
|
||||
|
||||
async canActivate() {
|
||||
const redirectUrl = (await this.stateService.getIsAuthenticated())
|
||||
? [this.homepage]
|
||||
: [this.loginpage];
|
||||
|
||||
this.router.navigate(redirectUrl);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
|
||||
import { KeyConnectorService } from "@/jslib/common/src/abstractions/keyConnector.service";
|
||||
import { PasswordRepromptService as PasswordRepromptServiceAbstraction } from "@/jslib/common/src/abstractions/passwordReprompt.service";
|
||||
|
||||
import { PasswordRepromptComponent } from "../components/password-reprompt.component";
|
||||
|
||||
import { ModalService } from "./modal.service";
|
||||
|
||||
/**
|
||||
* Used to verify the user's Master Password for the "Master Password Re-prompt" feature only.
|
||||
* See UserVerificationService for any other situation where you need to verify the user's identity.
|
||||
*/
|
||||
@Injectable()
|
||||
export class PasswordRepromptService implements PasswordRepromptServiceAbstraction {
|
||||
protected component = PasswordRepromptComponent;
|
||||
|
||||
constructor(
|
||||
private modalService: ModalService,
|
||||
private keyConnectorService: KeyConnectorService,
|
||||
) {}
|
||||
|
||||
protectedFields() {
|
||||
return ["TOTP", "Password", "H_Field", "Card Number", "Security Code"];
|
||||
}
|
||||
|
||||
async showPasswordPrompt() {
|
||||
if (!(await this.enabled())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const ref = this.modalService.open(this.component, { allowMultipleModals: true });
|
||||
|
||||
if (ref == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const result = await ref.onClosedPromise();
|
||||
return result === true;
|
||||
}
|
||||
|
||||
async enabled() {
|
||||
return !(await this.keyConnectorService.getUsesKeyConnector());
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
|
||||
import { StateService } from "@/jslib/common/src/abstractions/state.service";
|
||||
|
||||
@Injectable()
|
||||
export class UnauthGuardService {
|
||||
protected homepage = "vault";
|
||||
constructor(
|
||||
private router: Router,
|
||||
private stateService: StateService,
|
||||
) {}
|
||||
|
||||
async canActivate() {
|
||||
const isAuthed = await this.stateService.getIsAuthenticated();
|
||||
if (isAuthed) {
|
||||
this.router.navigate([this.homepage]);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { EventType } from "../enums/eventType";
|
||||
|
||||
export abstract class EventService {
|
||||
collect: (eventType: EventType, cipherId?: string, uploadImmediately?: boolean) => Promise<any>;
|
||||
uploadEvents: (userId?: string) => Promise<any>;
|
||||
clearEvents: (userId?: string) => Promise<any>;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
export abstract class NotificationsService {
|
||||
init: () => Promise<void>;
|
||||
updateConnection: (sync?: boolean) => Promise<void>;
|
||||
reconnectFromActivity: () => Promise<void>;
|
||||
disconnectFromInactivity: () => Promise<void>;
|
||||
}
|
||||
@@ -10,6 +10,7 @@ export abstract class PasswordGenerationService {
|
||||
enforcePasswordGeneratorPoliciesOnOptions: (
|
||||
options: any,
|
||||
) => Promise<[any, PasswordGeneratorPolicyOptions]>;
|
||||
getPasswordGeneratorPolicyOptions: () => Promise<PasswordGeneratorPolicyOptions>;
|
||||
saveOptions: (options: any) => Promise<any>;
|
||||
getHistory: () => Promise<GeneratedPasswordHistory[]>;
|
||||
addHistory: (password: string) => Promise<any>;
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
export abstract class PasswordRepromptService {
|
||||
protectedFields: () => string[];
|
||||
showPasswordPrompt: () => Promise<boolean>;
|
||||
enabled: () => Promise<boolean>;
|
||||
}
|
||||
32
jslib/common/src/abstractions/policy.service.ts
Normal file
32
jslib/common/src/abstractions/policy.service.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { PolicyType } from "../enums/policyType";
|
||||
import { PolicyData } from "../models/data/policyData";
|
||||
import { MasterPasswordPolicyOptions } from "../models/domain/masterPasswordPolicyOptions";
|
||||
import { Policy } from "../models/domain/policy";
|
||||
import { ResetPasswordPolicyOptions } from "../models/domain/resetPasswordPolicyOptions";
|
||||
import { ListResponse } from "../models/response/listResponse";
|
||||
import { PolicyResponse } from "../models/response/policyResponse";
|
||||
|
||||
export abstract class PolicyService {
|
||||
clearCache: () => void;
|
||||
getAll: (type?: PolicyType, userId?: string) => Promise<Policy[]>;
|
||||
getPolicyForOrganization: (policyType: PolicyType, organizationId: string) => Promise<Policy>;
|
||||
replace: (policies: { [id: string]: PolicyData }) => Promise<any>;
|
||||
clear: (userId?: string) => Promise<any>;
|
||||
getMasterPasswordPoliciesForInvitedUsers: (orgId: string) => Promise<MasterPasswordPolicyOptions>;
|
||||
getMasterPasswordPolicyOptions: (policies?: Policy[]) => Promise<MasterPasswordPolicyOptions>;
|
||||
evaluateMasterPassword: (
|
||||
passwordStrength: number,
|
||||
newPassword: string,
|
||||
enforcedPolicyOptions?: MasterPasswordPolicyOptions,
|
||||
) => boolean;
|
||||
getResetPasswordPolicyOptions: (
|
||||
policies: Policy[],
|
||||
orgId: string,
|
||||
) => [ResetPasswordPolicyOptions, boolean];
|
||||
mapPoliciesFromToken: (policiesResponse: ListResponse<PolicyResponse>) => Policy[];
|
||||
policyAppliesToUser: (
|
||||
policyType: PolicyType,
|
||||
policyFilter?: (policy: Policy) => boolean,
|
||||
userId?: string,
|
||||
) => Promise<boolean>;
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { CipherView } from "../models/view/cipherView";
|
||||
import { SendView } from "../models/view/sendView";
|
||||
|
||||
export abstract class SearchService {
|
||||
indexedEntityId?: string = null;
|
||||
clearIndex: () => void;
|
||||
isSearchable: (query: string) => boolean;
|
||||
indexCiphers: (indexedEntityGuid?: string, ciphersToIndex?: CipherView[]) => Promise<void>;
|
||||
searchCiphers: (
|
||||
query: string,
|
||||
filter?: ((cipher: CipherView) => boolean) | ((cipher: CipherView) => boolean)[],
|
||||
ciphers?: CipherView[],
|
||||
) => Promise<CipherView[]>;
|
||||
searchCiphersBasic: (ciphers: CipherView[], query: string, deleted?: boolean) => CipherView[];
|
||||
searchSends: (sends: SendView[], query: string) => SendView[];
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
export abstract class SettingsService {
|
||||
clearCache: () => Promise<void>;
|
||||
getEquivalentDomains: () => Promise<any>;
|
||||
setEquivalentDomains: (equivalentDomains: string[][]) => Promise<any>;
|
||||
clear: (userId?: string) => Promise<void>;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import {
|
||||
} from "../models/response/notificationResponse";
|
||||
|
||||
export abstract class SyncService {
|
||||
syncInProgress: boolean;
|
||||
|
||||
getLastSync: () => Promise<Date>;
|
||||
setLastSync: (date: Date, userId?: string) => Promise<any>;
|
||||
fullSync: (forceSync: boolean, allowThrowOnError?: boolean) => Promise<boolean>;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
export abstract class SystemService {
|
||||
startProcessReload: () => Promise<void>;
|
||||
cancelProcessReload: () => void;
|
||||
clearClipboard: (clipboardValue: string, timeoutMs?: number) => Promise<void>;
|
||||
clearPendingClipboard: () => Promise<any>;
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { SecretVerificationRequest } from "../models/request/secretVerificationRequest";
|
||||
import { Verification } from "../types/verification";
|
||||
|
||||
export abstract class UserVerificationService {
|
||||
buildRequest: <T extends SecretVerificationRequest>(
|
||||
verification: Verification,
|
||||
requestClass?: new () => T,
|
||||
alreadyHashed?: boolean,
|
||||
) => Promise<T>;
|
||||
verifyUser: (verification: Verification) => Promise<boolean>;
|
||||
requestOTP: () => Promise<void>;
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import { I18nService } from "../abstractions/i18n.service";
|
||||
|
||||
import * as tldjs from "tldjs";
|
||||
|
||||
|
||||
const nodeURL = typeof window === "undefined" ? require("url") : null;
|
||||
|
||||
export class Utils {
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
import { ApiService } from "../abstractions/api.service";
|
||||
import { EventService as EventServiceAbstraction } from "../abstractions/event.service";
|
||||
import { LogService } from "../abstractions/log.service";
|
||||
import { OrganizationService } from "../abstractions/organization.service";
|
||||
import { StateService } from "../abstractions/state.service";
|
||||
import { EventType } from "../enums/eventType";
|
||||
import { EventData } from "../models/data/eventData";
|
||||
import { EventRequest } from "../models/request/eventRequest";
|
||||
|
||||
export class EventService implements EventServiceAbstraction {
|
||||
private inited = false;
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private stateService: StateService,
|
||||
private logService: LogService,
|
||||
private organizationService: OrganizationService,
|
||||
) {}
|
||||
|
||||
init(checkOnInterval: boolean) {
|
||||
if (this.inited) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.inited = true;
|
||||
if (checkOnInterval) {
|
||||
this.uploadEvents();
|
||||
setInterval(() => this.uploadEvents(), 60 * 1000); // check every 60 seconds
|
||||
}
|
||||
}
|
||||
|
||||
async collect(
|
||||
eventType: EventType,
|
||||
cipherId: string = null,
|
||||
uploadImmediately = false,
|
||||
): Promise<any> {
|
||||
const authed = await this.stateService.getIsAuthenticated();
|
||||
if (!authed) {
|
||||
return;
|
||||
}
|
||||
const organizations = await this.organizationService.getAll();
|
||||
if (organizations == null) {
|
||||
return;
|
||||
}
|
||||
const orgIds = new Set<string>(organizations.filter((o) => o.useEvents).map((o) => o.id));
|
||||
if (orgIds.size === 0) {
|
||||
return;
|
||||
}
|
||||
let eventCollection = await this.stateService.getEventCollection();
|
||||
if (eventCollection == null) {
|
||||
eventCollection = [];
|
||||
}
|
||||
const event = new EventData();
|
||||
event.type = eventType;
|
||||
event.cipherId = cipherId;
|
||||
event.date = new Date().toISOString();
|
||||
eventCollection.push(event);
|
||||
await this.stateService.setEventCollection(eventCollection);
|
||||
if (uploadImmediately) {
|
||||
await this.uploadEvents();
|
||||
}
|
||||
}
|
||||
|
||||
async uploadEvents(userId?: string): Promise<any> {
|
||||
const authed = await this.stateService.getIsAuthenticated({ userId: userId });
|
||||
if (!authed) {
|
||||
return;
|
||||
}
|
||||
const eventCollection = await this.stateService.getEventCollection({ userId: userId });
|
||||
if (eventCollection == null || eventCollection.length === 0) {
|
||||
return;
|
||||
}
|
||||
const request = eventCollection.map((e) => {
|
||||
const req = new EventRequest();
|
||||
req.type = e.type;
|
||||
req.cipherId = e.cipherId;
|
||||
req.date = e.date;
|
||||
return req;
|
||||
});
|
||||
try {
|
||||
await this.apiService.postEventsCollect(request);
|
||||
this.clearEvents(userId);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async clearEvents(userId?: string): Promise<any> {
|
||||
await this.stateService.setEventCollection(null, { userId: userId });
|
||||
}
|
||||
}
|
||||
@@ -1,191 +0,0 @@
|
||||
import * as signalR from "@microsoft/signalr";
|
||||
import * as signalRMsgPack from "@microsoft/signalr-protocol-msgpack";
|
||||
|
||||
import { ApiService } from "../abstractions/api.service";
|
||||
import { AppIdService } from "../abstractions/appId.service";
|
||||
import { EnvironmentService } from "../abstractions/environment.service";
|
||||
import { LogService } from "../abstractions/log.service";
|
||||
import { NotificationsService as NotificationsServiceAbstraction } from "../abstractions/notifications.service";
|
||||
import { StateService } from "../abstractions/state.service";
|
||||
import { SyncService } from "../abstractions/sync.service";
|
||||
import { NotificationType } from "../enums/notificationType";
|
||||
import {
|
||||
NotificationResponse,
|
||||
} from "../models/response/notificationResponse";
|
||||
|
||||
export class NotificationsService implements NotificationsServiceAbstraction {
|
||||
private signalrConnection: signalR.HubConnection;
|
||||
private url: string;
|
||||
private connected = false;
|
||||
private inited = false;
|
||||
private inactive = false;
|
||||
private reconnectTimer: any = null;
|
||||
|
||||
constructor(
|
||||
private syncService: SyncService,
|
||||
private appIdService: AppIdService,
|
||||
private apiService: ApiService,
|
||||
private environmentService: EnvironmentService,
|
||||
private logoutCallback: () => Promise<void>,
|
||||
private logService: LogService,
|
||||
private stateService: StateService,
|
||||
) {
|
||||
this.environmentService.urls.subscribe(() => {
|
||||
if (!this.inited) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.init();
|
||||
});
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
this.inited = false;
|
||||
this.url = this.environmentService.getNotificationsUrl();
|
||||
|
||||
// Set notifications server URL to `https://-` to effectively disable communication
|
||||
// with the notifications server from the client app
|
||||
if (this.url === "https://-") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.signalrConnection != null) {
|
||||
this.signalrConnection.off("ReceiveMessage");
|
||||
this.signalrConnection.off("Heartbeat");
|
||||
await this.signalrConnection.stop();
|
||||
this.connected = false;
|
||||
this.signalrConnection = null;
|
||||
}
|
||||
|
||||
this.signalrConnection = new signalR.HubConnectionBuilder()
|
||||
.withUrl(this.url + "/hub", {
|
||||
accessTokenFactory: () => this.apiService.getActiveBearerToken(),
|
||||
skipNegotiation: true,
|
||||
transport: signalR.HttpTransportType.WebSockets,
|
||||
})
|
||||
.withHubProtocol(new signalRMsgPack.MessagePackHubProtocol() as signalR.IHubProtocol)
|
||||
// .configureLogging(signalR.LogLevel.Trace)
|
||||
.build();
|
||||
|
||||
this.signalrConnection.on("ReceiveMessage", (data: any) =>
|
||||
this.processNotification(new NotificationResponse(data)),
|
||||
);
|
||||
// eslint-disable-next-line
|
||||
this.signalrConnection.on("Heartbeat", (data: any) => {
|
||||
/*console.log('Heartbeat!');*/
|
||||
});
|
||||
this.signalrConnection.onclose(() => {
|
||||
this.connected = false;
|
||||
this.reconnect(true);
|
||||
});
|
||||
this.inited = true;
|
||||
if (await this.isAuthedAndUnlocked()) {
|
||||
await this.reconnect(false);
|
||||
}
|
||||
}
|
||||
|
||||
async updateConnection(sync = false): Promise<void> {
|
||||
if (!this.inited) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (await this.isAuthedAndUnlocked()) {
|
||||
await this.reconnect(sync);
|
||||
} else {
|
||||
await this.signalrConnection.stop();
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.error(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
async reconnectFromActivity(): Promise<void> {
|
||||
this.inactive = false;
|
||||
if (this.inited && !this.connected) {
|
||||
await this.reconnect(true);
|
||||
}
|
||||
}
|
||||
|
||||
async disconnectFromInactivity(): Promise<void> {
|
||||
this.inactive = true;
|
||||
if (this.inited && this.connected) {
|
||||
await this.signalrConnection.stop();
|
||||
}
|
||||
}
|
||||
|
||||
private async processNotification(notification: NotificationResponse) {
|
||||
const appId = await this.appIdService.getAppId();
|
||||
if (notification == null || notification.contextId === appId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isAuthenticated = await this.stateService.getIsAuthenticated();
|
||||
const payloadUserId = notification.payload.userId || notification.payload.UserId;
|
||||
const myUserId = await this.stateService.getUserId();
|
||||
if (isAuthenticated && payloadUserId != null && payloadUserId !== myUserId) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (notification.type) {
|
||||
case NotificationType.SyncVault:
|
||||
case NotificationType.SyncCiphers:
|
||||
case NotificationType.SyncSettings:
|
||||
if (isAuthenticated) {
|
||||
await this.syncService.fullSync(false);
|
||||
}
|
||||
break;
|
||||
case NotificationType.SyncOrgKeys:
|
||||
if (isAuthenticated) {
|
||||
await this.syncService.fullSync(true);
|
||||
// Stop so a reconnect can be made
|
||||
await this.signalrConnection.stop();
|
||||
}
|
||||
break;
|
||||
case NotificationType.LogOut:
|
||||
if (isAuthenticated) {
|
||||
this.logoutCallback();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async reconnect(sync: boolean) {
|
||||
if (this.reconnectTimer != null) {
|
||||
clearTimeout(this.reconnectTimer);
|
||||
this.reconnectTimer = null;
|
||||
}
|
||||
if (this.connected || !this.inited || this.inactive) {
|
||||
return;
|
||||
}
|
||||
const authedAndUnlocked = await this.isAuthedAndUnlocked();
|
||||
if (!authedAndUnlocked) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.signalrConnection.start();
|
||||
this.connected = true;
|
||||
if (sync) {
|
||||
await this.syncService.fullSync(false);
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
|
||||
if (!this.connected) {
|
||||
this.reconnectTimer = setTimeout(() => this.reconnect(sync), this.random(120000, 300000));
|
||||
}
|
||||
}
|
||||
|
||||
private async isAuthedAndUnlocked() {
|
||||
return await this.stateService.getIsAuthenticated()
|
||||
}
|
||||
|
||||
private random(min: number, max: number) {
|
||||
min = Math.ceil(min);
|
||||
max = Math.floor(max);
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,14 @@ import * as zxcvbn from "zxcvbn";
|
||||
|
||||
import { CryptoService } from "../abstractions/crypto.service";
|
||||
import { PasswordGenerationService as PasswordGenerationServiceAbstraction } from "../abstractions/passwordGeneration.service";
|
||||
import { PolicyService } from "../abstractions/policy.service";
|
||||
import { StateService } from "../abstractions/state.service";
|
||||
import { PolicyType } from "../enums/policyType";
|
||||
import { EEFLongWordList } from "../misc/wordlist";
|
||||
import { EncString } from "../models/domain/encString";
|
||||
import { GeneratedPasswordHistory } from "../models/domain/generatedPasswordHistory";
|
||||
import { PasswordGeneratorPolicyOptions } from "../models/domain/passwordGeneratorPolicyOptions";
|
||||
import { Policy } from "../models/domain/policy";
|
||||
|
||||
const DefaultOptions = {
|
||||
length: 14,
|
||||
@@ -31,6 +34,7 @@ const MaxPasswordsInHistory = 100;
|
||||
export class PasswordGenerationService implements PasswordGenerationServiceAbstraction {
|
||||
constructor(
|
||||
private cryptoService: CryptoService,
|
||||
private policyService: PolicyService,
|
||||
private stateService: StateService,
|
||||
) {}
|
||||
|
||||
@@ -189,7 +193,146 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr
|
||||
async enforcePasswordGeneratorPoliciesOnOptions(
|
||||
options: any,
|
||||
): Promise<[any, PasswordGeneratorPolicyOptions]> {
|
||||
return [options, new PasswordGeneratorPolicyOptions()];
|
||||
let enforcedPolicyOptions = await this.getPasswordGeneratorPolicyOptions();
|
||||
if (enforcedPolicyOptions != null) {
|
||||
if (options.length < enforcedPolicyOptions.minLength) {
|
||||
options.length = enforcedPolicyOptions.minLength;
|
||||
}
|
||||
|
||||
if (enforcedPolicyOptions.useUppercase) {
|
||||
options.uppercase = true;
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
async getPasswordGeneratorPolicyOptions(): Promise<PasswordGeneratorPolicyOptions> {
|
||||
const policies: Policy[] =
|
||||
this.policyService == null
|
||||
? null
|
||||
: await this.policyService.getAll(PolicyType.PasswordGenerator);
|
||||
let enforcedOptions: PasswordGeneratorPolicyOptions = null;
|
||||
|
||||
if (policies == null || policies.length === 0) {
|
||||
return enforcedOptions;
|
||||
}
|
||||
|
||||
policies.forEach((currentPolicy) => {
|
||||
if (!currentPolicy.enabled || currentPolicy.data == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (enforcedOptions == null) {
|
||||
enforcedOptions = new PasswordGeneratorPolicyOptions();
|
||||
}
|
||||
|
||||
// Password wins in multi-org collisions
|
||||
if (currentPolicy.data.defaultType != null && enforcedOptions.defaultType !== "password") {
|
||||
enforcedOptions.defaultType = currentPolicy.data.defaultType;
|
||||
}
|
||||
|
||||
if (
|
||||
currentPolicy.data.minLength != null &&
|
||||
currentPolicy.data.minLength > enforcedOptions.minLength
|
||||
) {
|
||||
enforcedOptions.minLength = currentPolicy.data.minLength;
|
||||
}
|
||||
|
||||
if (currentPolicy.data.useUpper) {
|
||||
enforcedOptions.useUppercase = true;
|
||||
}
|
||||
|
||||
if (currentPolicy.data.useLower) {
|
||||
enforcedOptions.useLowercase = true;
|
||||
}
|
||||
|
||||
if (currentPolicy.data.useNumbers) {
|
||||
enforcedOptions.useNumbers = true;
|
||||
}
|
||||
|
||||
if (
|
||||
currentPolicy.data.minNumbers != null &&
|
||||
currentPolicy.data.minNumbers > enforcedOptions.numberCount
|
||||
) {
|
||||
enforcedOptions.numberCount = currentPolicy.data.minNumbers;
|
||||
}
|
||||
|
||||
if (currentPolicy.data.useSpecial) {
|
||||
enforcedOptions.useSpecial = true;
|
||||
}
|
||||
|
||||
if (
|
||||
currentPolicy.data.minSpecial != null &&
|
||||
currentPolicy.data.minSpecial > enforcedOptions.specialCount
|
||||
) {
|
||||
enforcedOptions.specialCount = currentPolicy.data.minSpecial;
|
||||
}
|
||||
|
||||
if (
|
||||
currentPolicy.data.minNumberWords != null &&
|
||||
currentPolicy.data.minNumberWords > enforcedOptions.minNumberWords
|
||||
) {
|
||||
enforcedOptions.minNumberWords = currentPolicy.data.minNumberWords;
|
||||
}
|
||||
|
||||
if (currentPolicy.data.capitalize) {
|
||||
enforcedOptions.capitalize = true;
|
||||
}
|
||||
|
||||
if (currentPolicy.data.includeNumber) {
|
||||
enforcedOptions.includeNumber = true;
|
||||
}
|
||||
});
|
||||
|
||||
return enforcedOptions;
|
||||
}
|
||||
|
||||
async saveOptions(options: any) {
|
||||
|
||||
247
jslib/common/src/services/policy.service.ts
Normal file
247
jslib/common/src/services/policy.service.ts
Normal file
@@ -0,0 +1,247 @@
|
||||
import { ApiService } from "../abstractions/api.service";
|
||||
import { OrganizationService } from "../abstractions/organization.service";
|
||||
import { PolicyService as PolicyServiceAbstraction } from "../abstractions/policy.service";
|
||||
import { StateService } from "../abstractions/state.service";
|
||||
import { OrganizationUserStatusType } from "../enums/organizationUserStatusType";
|
||||
import { OrganizationUserType } from "../enums/organizationUserType";
|
||||
import { PolicyType } from "../enums/policyType";
|
||||
import { PolicyData } from "../models/data/policyData";
|
||||
import { MasterPasswordPolicyOptions } from "../models/domain/masterPasswordPolicyOptions";
|
||||
import { Organization } from "../models/domain/organization";
|
||||
import { Policy } from "../models/domain/policy";
|
||||
import { ResetPasswordPolicyOptions } from "../models/domain/resetPasswordPolicyOptions";
|
||||
import { ListResponse } from "../models/response/listResponse";
|
||||
import { PolicyResponse } from "../models/response/policyResponse";
|
||||
|
||||
export class PolicyService implements PolicyServiceAbstraction {
|
||||
policyCache: Policy[];
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
private organizationService: OrganizationService,
|
||||
private apiService: ApiService,
|
||||
) {}
|
||||
|
||||
async clearCache(): Promise<void> {
|
||||
await this.stateService.setDecryptedPolicies(null);
|
||||
}
|
||||
|
||||
async getAll(type?: PolicyType, userId?: string): Promise<Policy[]> {
|
||||
let response: Policy[] = [];
|
||||
const decryptedPolicies = await this.stateService.getDecryptedPolicies({ userId: userId });
|
||||
if (decryptedPolicies != null) {
|
||||
response = decryptedPolicies;
|
||||
} else {
|
||||
const diskPolicies = await this.stateService.getEncryptedPolicies({ userId: userId });
|
||||
for (const id in diskPolicies) {
|
||||
// eslint-disable-next-line
|
||||
if (diskPolicies.hasOwnProperty(id)) {
|
||||
response.push(new Policy(diskPolicies[id]));
|
||||
}
|
||||
}
|
||||
await this.stateService.setDecryptedPolicies(response, { userId: userId });
|
||||
}
|
||||
if (type != null) {
|
||||
return response.filter((policy) => policy.type === type);
|
||||
} else {
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
async getPolicyForOrganization(policyType: PolicyType, organizationId: string): Promise<Policy> {
|
||||
const org = await this.organizationService.get(organizationId);
|
||||
if (org?.isProviderUser) {
|
||||
const orgPolicies = await this.apiService.getPolicies(organizationId);
|
||||
const policy = orgPolicies.data.find((p) => p.organizationId === organizationId);
|
||||
|
||||
if (policy == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Policy(new PolicyData(policy));
|
||||
}
|
||||
|
||||
const policies = await this.getAll(policyType);
|
||||
return policies.find((p) => p.organizationId === organizationId);
|
||||
}
|
||||
|
||||
async replace(policies: { [id: string]: PolicyData }): Promise<any> {
|
||||
await this.stateService.setDecryptedPolicies(null);
|
||||
await this.stateService.setEncryptedPolicies(policies);
|
||||
}
|
||||
|
||||
async clear(userId?: string): Promise<any> {
|
||||
await this.stateService.setDecryptedPolicies(null, { userId: userId });
|
||||
await this.stateService.setEncryptedPolicies(null, { userId: userId });
|
||||
}
|
||||
|
||||
async getMasterPasswordPoliciesForInvitedUsers(
|
||||
orgId: string,
|
||||
): Promise<MasterPasswordPolicyOptions> {
|
||||
const userId = await this.stateService.getUserId();
|
||||
const response = await this.apiService.getPoliciesByInvitedUser(orgId, userId);
|
||||
const policies = await this.mapPoliciesFromToken(response);
|
||||
return this.getMasterPasswordPolicyOptions(policies);
|
||||
}
|
||||
|
||||
async getMasterPasswordPolicyOptions(policies?: Policy[]): Promise<MasterPasswordPolicyOptions> {
|
||||
let enforcedOptions: MasterPasswordPolicyOptions = null;
|
||||
|
||||
if (policies == null) {
|
||||
policies = await this.getAll(PolicyType.MasterPassword);
|
||||
} else {
|
||||
policies = policies.filter((p) => p.type === PolicyType.MasterPassword);
|
||||
}
|
||||
|
||||
if (policies == null || policies.length === 0) {
|
||||
return enforcedOptions;
|
||||
}
|
||||
|
||||
policies.forEach((currentPolicy) => {
|
||||
if (!currentPolicy.enabled || currentPolicy.data == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (enforcedOptions == null) {
|
||||
enforcedOptions = new MasterPasswordPolicyOptions();
|
||||
}
|
||||
|
||||
if (
|
||||
currentPolicy.data.minComplexity != null &&
|
||||
currentPolicy.data.minComplexity > enforcedOptions.minComplexity
|
||||
) {
|
||||
enforcedOptions.minComplexity = currentPolicy.data.minComplexity;
|
||||
}
|
||||
|
||||
if (
|
||||
currentPolicy.data.minLength != null &&
|
||||
currentPolicy.data.minLength > enforcedOptions.minLength
|
||||
) {
|
||||
enforcedOptions.minLength = currentPolicy.data.minLength;
|
||||
}
|
||||
|
||||
if (currentPolicy.data.requireUpper) {
|
||||
enforcedOptions.requireUpper = true;
|
||||
}
|
||||
|
||||
if (currentPolicy.data.requireLower) {
|
||||
enforcedOptions.requireLower = true;
|
||||
}
|
||||
|
||||
if (currentPolicy.data.requireNumbers) {
|
||||
enforcedOptions.requireNumbers = true;
|
||||
}
|
||||
|
||||
if (currentPolicy.data.requireSpecial) {
|
||||
enforcedOptions.requireSpecial = true;
|
||||
}
|
||||
});
|
||||
|
||||
return enforcedOptions;
|
||||
}
|
||||
|
||||
evaluateMasterPassword(
|
||||
passwordStrength: number,
|
||||
newPassword: string,
|
||||
enforcedPolicyOptions: MasterPasswordPolicyOptions,
|
||||
): boolean {
|
||||
if (enforcedPolicyOptions == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
enforcedPolicyOptions.minComplexity > 0 &&
|
||||
enforcedPolicyOptions.minComplexity > passwordStrength
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
enforcedPolicyOptions.minLength > 0 &&
|
||||
enforcedPolicyOptions.minLength > newPassword.length
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (enforcedPolicyOptions.requireUpper && newPassword.toLocaleLowerCase() === newPassword) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (enforcedPolicyOptions.requireLower && newPassword.toLocaleUpperCase() === newPassword) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (enforcedPolicyOptions.requireNumbers && !/[0-9]/.test(newPassword)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
if (enforcedPolicyOptions.requireSpecial && !/[!@#$%\^&*]/g.test(newPassword)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
getResetPasswordPolicyOptions(
|
||||
policies: Policy[],
|
||||
orgId: string,
|
||||
): [ResetPasswordPolicyOptions, boolean] {
|
||||
const resetPasswordPolicyOptions = new ResetPasswordPolicyOptions();
|
||||
|
||||
if (policies == null || orgId == null) {
|
||||
return [resetPasswordPolicyOptions, false];
|
||||
}
|
||||
|
||||
const policy = policies.find(
|
||||
(p) => p.organizationId === orgId && p.type === PolicyType.ResetPassword && p.enabled,
|
||||
);
|
||||
resetPasswordPolicyOptions.autoEnrollEnabled = policy?.data?.autoEnrollEnabled ?? false;
|
||||
|
||||
return [resetPasswordPolicyOptions, policy?.enabled ?? false];
|
||||
}
|
||||
|
||||
mapPoliciesFromToken(policiesResponse: ListResponse<PolicyResponse>): Policy[] {
|
||||
if (policiesResponse == null || policiesResponse.data == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const policiesData = policiesResponse.data.map((p) => new PolicyData(p));
|
||||
return policiesData.map((p) => new Policy(p));
|
||||
}
|
||||
|
||||
async policyAppliesToUser(
|
||||
policyType: PolicyType,
|
||||
policyFilter?: (policy: Policy) => boolean,
|
||||
userId?: string,
|
||||
) {
|
||||
const policies = await this.getAll(policyType, userId);
|
||||
const organizations = await this.organizationService.getAll(userId);
|
||||
let filteredPolicies;
|
||||
|
||||
if (policyFilter != null) {
|
||||
filteredPolicies = policies.filter((p) => p.enabled && policyFilter(p));
|
||||
} else {
|
||||
filteredPolicies = policies.filter((p) => p.enabled);
|
||||
}
|
||||
|
||||
const policySet = new Set(filteredPolicies.map((p) => p.organizationId));
|
||||
|
||||
return organizations.some(
|
||||
(o) =>
|
||||
o.enabled &&
|
||||
o.status >= OrganizationUserStatusType.Accepted &&
|
||||
o.usePolicies &&
|
||||
!this.isExcemptFromPolicies(o, policyType) &&
|
||||
policySet.has(o.id),
|
||||
);
|
||||
}
|
||||
|
||||
private isExcemptFromPolicies(organization: Organization, policyType: PolicyType) {
|
||||
if (policyType === PolicyType.MaximumVaultTimeout) {
|
||||
return organization.type === OrganizationUserType.Owner;
|
||||
}
|
||||
|
||||
return organization.isExemptFromPolicies;
|
||||
}
|
||||
}
|
||||
@@ -1,276 +0,0 @@
|
||||
import * as lunr from "lunr";
|
||||
|
||||
import { I18nService } from "../abstractions/i18n.service";
|
||||
import { LogService } from "../abstractions/log.service";
|
||||
import { SearchService as SearchServiceAbstraction } from "../abstractions/search.service";
|
||||
import { CipherType } from "../enums/cipherType";
|
||||
import { FieldType } from "../enums/fieldType";
|
||||
import { UriMatchType } from "../enums/uriMatchType";
|
||||
import { CipherView } from "../models/view/cipherView";
|
||||
import { SendView } from "../models/view/sendView";
|
||||
|
||||
export class SearchService implements SearchServiceAbstraction {
|
||||
indexedEntityId?: string = null;
|
||||
private indexing = false;
|
||||
private index: lunr.Index = null;
|
||||
private searchableMinLength = 2;
|
||||
|
||||
constructor(
|
||||
private logService: LogService,
|
||||
private i18nService: I18nService,
|
||||
) {
|
||||
if (["zh-CN", "zh-TW"].indexOf(i18nService.locale) !== -1) {
|
||||
this.searchableMinLength = 1;
|
||||
}
|
||||
}
|
||||
|
||||
clearIndex(): void {
|
||||
this.indexedEntityId = null;
|
||||
this.index = null;
|
||||
}
|
||||
|
||||
isSearchable(query: string): boolean {
|
||||
const notSearchable =
|
||||
query == null ||
|
||||
(this.index == null && query.length < this.searchableMinLength) ||
|
||||
(this.index != null && query.length < this.searchableMinLength && query.indexOf(">") !== 0);
|
||||
return !notSearchable;
|
||||
}
|
||||
|
||||
async indexCiphers(indexedEntityId?: string, ciphers?: CipherView[]): Promise<void> {
|
||||
if (this.indexing) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.logService.time("search indexing");
|
||||
this.indexing = true;
|
||||
this.indexedEntityId = indexedEntityId;
|
||||
this.index = null;
|
||||
const builder = new lunr.Builder();
|
||||
builder.ref("id");
|
||||
builder.field("shortid", { boost: 100, extractor: (c: CipherView) => c.id.substr(0, 8) });
|
||||
builder.field("name", { boost: 10 });
|
||||
builder.field("subtitle", {
|
||||
boost: 5,
|
||||
extractor: (c: CipherView) => {
|
||||
if (c.subTitle != null && c.type === CipherType.Card) {
|
||||
return c.subTitle.replace(/\*/g, "");
|
||||
}
|
||||
return c.subTitle;
|
||||
},
|
||||
});
|
||||
builder.field("notes");
|
||||
builder.field("login.username", {
|
||||
extractor: (c: CipherView) =>
|
||||
c.type === CipherType.Login && c.login != null ? c.login.username : null,
|
||||
});
|
||||
builder.field("login.uris", { boost: 2, extractor: (c: CipherView) => this.uriExtractor(c) });
|
||||
builder.field("fields", { extractor: (c: CipherView) => this.fieldExtractor(c, false) });
|
||||
builder.field("fields_joined", { extractor: (c: CipherView) => this.fieldExtractor(c, true) });
|
||||
builder.field("attachments", {
|
||||
extractor: (c: CipherView) => this.attachmentExtractor(c, false),
|
||||
});
|
||||
builder.field("attachments_joined", {
|
||||
extractor: (c: CipherView) => this.attachmentExtractor(c, true),
|
||||
});
|
||||
builder.field("organizationid", { extractor: (c: CipherView) => c.organizationId });
|
||||
ciphers.forEach((c) => builder.add(c));
|
||||
this.index = builder.build();
|
||||
|
||||
this.indexing = false;
|
||||
|
||||
this.logService.timeEnd("search indexing");
|
||||
}
|
||||
|
||||
async searchCiphers(
|
||||
query: string,
|
||||
filter: ((cipher: CipherView) => boolean) | ((cipher: CipherView) => boolean)[] = null,
|
||||
ciphers: CipherView[] = null,
|
||||
): Promise<CipherView[]> {
|
||||
const results: CipherView[] = [];
|
||||
if (query != null) {
|
||||
query = query.trim().toLowerCase();
|
||||
}
|
||||
if (query === "") {
|
||||
query = null;
|
||||
}
|
||||
if (filter != null && Array.isArray(filter) && filter.length > 0) {
|
||||
ciphers = ciphers.filter((c) => filter.every((f) => f == null || f(c)));
|
||||
} else if (filter != null) {
|
||||
ciphers = ciphers.filter(filter as (cipher: CipherView) => boolean);
|
||||
}
|
||||
|
||||
if (!this.isSearchable(query)) {
|
||||
return ciphers;
|
||||
}
|
||||
|
||||
if (this.indexing) {
|
||||
await new Promise((r) => setTimeout(r, 250));
|
||||
if (this.indexing) {
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
}
|
||||
}
|
||||
|
||||
const index = this.getIndexForSearch();
|
||||
if (index == null) {
|
||||
// Fall back to basic search if index is not available
|
||||
return this.searchCiphersBasic(ciphers, query);
|
||||
}
|
||||
|
||||
const ciphersMap = new Map<string, CipherView>();
|
||||
ciphers.forEach((c) => ciphersMap.set(c.id, c));
|
||||
|
||||
let searchResults: lunr.Index.Result[] = null;
|
||||
const isQueryString = query != null && query.length > 1 && query.indexOf(">") === 0;
|
||||
if (isQueryString) {
|
||||
try {
|
||||
searchResults = index.search(query.substr(1).trim());
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
} else {
|
||||
const soWild = lunr.Query.wildcard.LEADING | lunr.Query.wildcard.TRAILING;
|
||||
searchResults = index.query((q) => {
|
||||
lunr.tokenizer(query).forEach((token) => {
|
||||
const t = token.toString();
|
||||
q.term(t, { fields: ["name"], wildcard: soWild });
|
||||
q.term(t, { fields: ["subtitle"], wildcard: soWild });
|
||||
q.term(t, { fields: ["login.uris"], wildcard: soWild });
|
||||
q.term(t, {});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (searchResults != null) {
|
||||
searchResults.forEach((r) => {
|
||||
if (ciphersMap.has(r.ref)) {
|
||||
results.push(ciphersMap.get(r.ref));
|
||||
}
|
||||
});
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
searchCiphersBasic(ciphers: CipherView[], query: string, deleted = false) {
|
||||
query = query.trim().toLowerCase();
|
||||
return ciphers.filter((c) => {
|
||||
if (deleted !== c.isDeleted) {
|
||||
return false;
|
||||
}
|
||||
if (c.name != null && c.name.toLowerCase().indexOf(query) > -1) {
|
||||
return true;
|
||||
}
|
||||
if (query.length >= 8 && c.id.startsWith(query)) {
|
||||
return true;
|
||||
}
|
||||
if (c.subTitle != null && c.subTitle.toLowerCase().indexOf(query) > -1) {
|
||||
return true;
|
||||
}
|
||||
if (c.login && c.login.uri != null && c.login.uri.toLowerCase().indexOf(query) > -1) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
searchSends(sends: SendView[], query: string) {
|
||||
query = query.trim().toLocaleLowerCase();
|
||||
|
||||
return sends.filter((s) => {
|
||||
if (s.name != null && s.name.toLowerCase().indexOf(query) > -1) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
query.length >= 8 &&
|
||||
(s.id.startsWith(query) ||
|
||||
s.accessId.toLocaleLowerCase().startsWith(query) ||
|
||||
(s.file?.id != null && s.file.id.startsWith(query)))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (s.notes != null && s.notes.toLowerCase().indexOf(query) > -1) {
|
||||
return true;
|
||||
}
|
||||
if (s.text?.text != null && s.text.text.toLowerCase().indexOf(query) > -1) {
|
||||
return true;
|
||||
}
|
||||
if (s.file?.fileName != null && s.file.fileName.toLowerCase().indexOf(query) > -1) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getIndexForSearch(): lunr.Index {
|
||||
return this.index;
|
||||
}
|
||||
|
||||
private fieldExtractor(c: CipherView, joined: boolean) {
|
||||
if (!c.hasFields) {
|
||||
return null;
|
||||
}
|
||||
let fields: string[] = [];
|
||||
c.fields.forEach((f) => {
|
||||
if (f.name != null) {
|
||||
fields.push(f.name);
|
||||
}
|
||||
if (f.type === FieldType.Text && f.value != null) {
|
||||
fields.push(f.value);
|
||||
}
|
||||
});
|
||||
fields = fields.filter((f) => f.trim() !== "");
|
||||
if (fields.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return joined ? fields.join(" ") : fields;
|
||||
}
|
||||
|
||||
private attachmentExtractor(c: CipherView, joined: boolean) {
|
||||
if (!c.hasAttachments) {
|
||||
return null;
|
||||
}
|
||||
let attachments: string[] = [];
|
||||
c.attachments.forEach((a) => {
|
||||
if (a != null && a.fileName != null) {
|
||||
if (joined && a.fileName.indexOf(".") > -1) {
|
||||
attachments.push(a.fileName.substr(0, a.fileName.lastIndexOf(".")));
|
||||
} else {
|
||||
attachments.push(a.fileName);
|
||||
}
|
||||
}
|
||||
});
|
||||
attachments = attachments.filter((f) => f.trim() !== "");
|
||||
if (attachments.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return joined ? attachments.join(" ") : attachments;
|
||||
}
|
||||
|
||||
private uriExtractor(c: CipherView) {
|
||||
if (c.type !== CipherType.Login || c.login == null || !c.login.hasUris) {
|
||||
return null;
|
||||
}
|
||||
const uris: string[] = [];
|
||||
c.login.uris.forEach((u) => {
|
||||
if (u.uri == null || u.uri === "") {
|
||||
return;
|
||||
}
|
||||
if (u.hostname != null) {
|
||||
uris.push(u.hostname);
|
||||
return;
|
||||
}
|
||||
let uri = u.uri;
|
||||
if (u.match !== UriMatchType.RegularExpression) {
|
||||
const protocolIndex = uri.indexOf("://");
|
||||
if (protocolIndex > -1) {
|
||||
uri = uri.substr(protocolIndex + 3);
|
||||
}
|
||||
const queryIndex = uri.search(/\?|&|#/);
|
||||
if (queryIndex > -1) {
|
||||
uri = uri.substring(0, queryIndex);
|
||||
}
|
||||
}
|
||||
uris.push(uri);
|
||||
});
|
||||
return uris.length > 0 ? uris : null;
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
import { SettingsService as SettingsServiceAbstraction } from "../abstractions/settings.service";
|
||||
import { StateService } from "../abstractions/state.service";
|
||||
|
||||
const Keys = {
|
||||
settingsPrefix: "settings_",
|
||||
equivalentDomains: "equivalentDomains",
|
||||
};
|
||||
|
||||
export class SettingsService implements SettingsServiceAbstraction {
|
||||
constructor(private stateService: StateService) {}
|
||||
|
||||
async clearCache(): Promise<void> {
|
||||
await this.stateService.setSettings(null);
|
||||
}
|
||||
|
||||
getEquivalentDomains(): Promise<any> {
|
||||
return this.getSettingsKey(Keys.equivalentDomains);
|
||||
}
|
||||
|
||||
async setEquivalentDomains(equivalentDomains: string[][]): Promise<void> {
|
||||
await this.setSettingsKey(Keys.equivalentDomains, equivalentDomains);
|
||||
}
|
||||
|
||||
async clear(userId?: string): Promise<void> {
|
||||
await this.stateService.setSettings(null, { userId: userId });
|
||||
}
|
||||
|
||||
// Helpers
|
||||
|
||||
private async getSettings(): Promise<any> {
|
||||
const settings = await this.stateService.getSettings();
|
||||
if (settings == null) {
|
||||
// eslint-disable-next-line
|
||||
const userId = await this.stateService.getUserId();
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
|
||||
private async getSettingsKey(key: string): Promise<any> {
|
||||
const settings = await this.getSettings();
|
||||
if (settings != null && settings[key]) {
|
||||
return settings[key];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private async setSettingsKey(key: string, value: any): Promise<void> {
|
||||
let settings = await this.getSettings();
|
||||
if (!settings) {
|
||||
settings = {};
|
||||
}
|
||||
|
||||
settings[key] = value;
|
||||
await this.stateService.setSettings(settings);
|
||||
}
|
||||
}
|
||||
@@ -2172,11 +2172,11 @@ export class StateService<
|
||||
}
|
||||
|
||||
const account = options?.useSecureStorage
|
||||
? (await this.secureStorageService.get<TAccount>(options.userId, options)) ??
|
||||
? ((await this.secureStorageService.get<TAccount>(options.userId, options)) ??
|
||||
(await this.storageService.get<TAccount>(
|
||||
options.userId,
|
||||
this.reconcileOptions(options, { htmlStorageLocation: HtmlStorageLocation.Local }),
|
||||
))
|
||||
)))
|
||||
: await this.storageService.get<TAccount>(options.userId, options);
|
||||
|
||||
if (this.useAccountCache) {
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
import { ApiService } from "../abstractions/api.service";
|
||||
import { CryptoService } from "../abstractions/crypto.service";
|
||||
import { KeyConnectorService } from "../abstractions/keyConnector.service";
|
||||
import { LogService } from "../abstractions/log.service";
|
||||
import { MessagingService } from "../abstractions/messaging.service";
|
||||
import { OrganizationService } from "../abstractions/organization.service";
|
||||
import { SettingsService } from "../abstractions/settings.service";
|
||||
import { StateService } from "../abstractions/state.service";
|
||||
import { SyncService as SyncServiceAbstraction } from "../abstractions/sync.service";
|
||||
import { sequentialize } from "../misc/sequentialize";
|
||||
import { OrganizationData } from "../models/data/organizationData";
|
||||
import { DomainsResponse } from "../models/response/domainsResponse";
|
||||
import { ProfileResponse } from "../models/response/profileResponse";
|
||||
|
||||
export class SyncService implements SyncServiceAbstraction {
|
||||
syncInProgress = false;
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private settingsService: SettingsService,
|
||||
private cryptoService: CryptoService,
|
||||
private messagingService: MessagingService,
|
||||
private logService: LogService,
|
||||
private keyConnectorService: KeyConnectorService,
|
||||
private stateService: StateService,
|
||||
private organizationService: OrganizationService,
|
||||
private logoutCallback: (expired: boolean) => Promise<void>,
|
||||
) {}
|
||||
|
||||
async getLastSync(): Promise<Date> {
|
||||
if ((await this.stateService.getUserId()) == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const lastSync = await this.stateService.getLastSync();
|
||||
if (lastSync) {
|
||||
return new Date(lastSync);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async setLastSync(date: Date, userId?: string): Promise<any> {
|
||||
await this.stateService.setLastSync(date.toJSON(), { userId: userId });
|
||||
}
|
||||
|
||||
@sequentialize(() => "fullSync")
|
||||
async fullSync(forceSync: boolean, allowThrowOnError = false): Promise<boolean> {
|
||||
this.syncStarted();
|
||||
const isAuthenticated = await this.stateService.getIsAuthenticated();
|
||||
if (!isAuthenticated) {
|
||||
return this.syncCompleted(false);
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
let needsSync = false;
|
||||
try {
|
||||
needsSync = await this.needsSyncing(forceSync);
|
||||
} catch (e) {
|
||||
if (allowThrowOnError) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
if (!needsSync) {
|
||||
await this.setLastSync(now);
|
||||
return this.syncCompleted(false);
|
||||
}
|
||||
|
||||
try {
|
||||
await this.apiService.refreshIdentityToken();
|
||||
const response = await this.apiService.getSync();
|
||||
|
||||
await this.syncProfile(response.profile);
|
||||
await this.syncSettings(response.domains);
|
||||
|
||||
await this.setLastSync(now);
|
||||
return this.syncCompleted(true);
|
||||
} catch (e) {
|
||||
if (allowThrowOnError) {
|
||||
throw e;
|
||||
} else {
|
||||
return this.syncCompleted(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helpers
|
||||
|
||||
private syncStarted() {
|
||||
this.syncInProgress = true;
|
||||
this.messagingService.send("syncStarted");
|
||||
}
|
||||
|
||||
private syncCompleted(successfully: boolean): boolean {
|
||||
this.syncInProgress = false;
|
||||
this.messagingService.send("syncCompleted", { successfully: successfully });
|
||||
return successfully;
|
||||
}
|
||||
|
||||
private async needsSyncing(forceSync: boolean) {
|
||||
if (forceSync) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const lastSync = await this.getLastSync();
|
||||
if (lastSync == null || lastSync.getTime() === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const response = await this.apiService.getAccountRevisionDate();
|
||||
if (new Date(response) <= lastSync) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private async syncProfile(response: ProfileResponse) {
|
||||
const stamp = await this.stateService.getSecurityStamp();
|
||||
if (stamp != null && stamp !== response.securityStamp) {
|
||||
if (this.logoutCallback != null) {
|
||||
await this.logoutCallback(true);
|
||||
}
|
||||
|
||||
throw new Error("Stamp has changed");
|
||||
}
|
||||
|
||||
await this.cryptoService.setEncKey(response.key);
|
||||
await this.cryptoService.setEncPrivateKey(response.privateKey);
|
||||
await this.cryptoService.setOrgKeys(response.organizations, response.providerOrganizations);
|
||||
await this.stateService.setSecurityStamp(response.securityStamp);
|
||||
await this.stateService.setEmailVerified(response.emailVerified);
|
||||
await this.stateService.setForcePasswordReset(response.forcePasswordReset);
|
||||
await this.keyConnectorService.setUsesKeyConnector(response.usesKeyConnector);
|
||||
|
||||
const organizations: { [id: string]: OrganizationData } = {};
|
||||
response.organizations.forEach((o) => {
|
||||
organizations[o.id] = new OrganizationData(o);
|
||||
});
|
||||
|
||||
response.providerOrganizations.forEach((o) => {
|
||||
if (organizations[o.id] == null) {
|
||||
organizations[o.id] = new OrganizationData(o);
|
||||
organizations[o.id].isProviderUser = true;
|
||||
}
|
||||
});
|
||||
|
||||
await this.organizationService.save(organizations);
|
||||
|
||||
if (await this.keyConnectorService.userNeedsMigration()) {
|
||||
await this.keyConnectorService.setConvertAccountRequired(true);
|
||||
this.messagingService.send("convertAccountToKeyConnector");
|
||||
} else {
|
||||
this.keyConnectorService.removeConvertAccountRequired();
|
||||
}
|
||||
}
|
||||
|
||||
private async syncSettings(response: DomainsResponse) {
|
||||
let eqDomains: string[][] = [];
|
||||
if (response != null && response.equivalentDomains != null) {
|
||||
eqDomains = eqDomains.concat(response.equivalentDomains);
|
||||
}
|
||||
|
||||
if (response != null && response.globalEquivalentDomains != null) {
|
||||
response.globalEquivalentDomains.forEach((global) => {
|
||||
if (global.domains.length > 0) {
|
||||
eqDomains.push(global.domains);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return this.settingsService.setEquivalentDomains(eqDomains);
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
import { MessagingService } from "../abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "../abstractions/platformUtils.service";
|
||||
import { StateService } from "../abstractions/state.service";
|
||||
import { SystemService as SystemServiceAbstraction } from "../abstractions/system.service";
|
||||
import { Utils } from "../misc/utils";
|
||||
|
||||
export class SystemService implements SystemServiceAbstraction {
|
||||
private reloadInterval: any = null;
|
||||
private clearClipboardTimeout: any = null;
|
||||
private clearClipboardTimeoutFunction: () => Promise<any> = null;
|
||||
|
||||
constructor(
|
||||
private messagingService: MessagingService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private reloadCallback: () => Promise<void> = null,
|
||||
private stateService: StateService,
|
||||
) {}
|
||||
|
||||
async startProcessReload(): Promise<void> {
|
||||
if (
|
||||
(await this.stateService.getDecryptedPinProtected()) != null ||
|
||||
(await this.stateService.getBiometricLocked()) ||
|
||||
this.reloadInterval != null
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.cancelProcessReload();
|
||||
this.reloadInterval = setInterval(async () => {
|
||||
let doRefresh = false;
|
||||
const lastActive = await this.stateService.getLastActive();
|
||||
if (lastActive != null) {
|
||||
const diffSeconds = new Date().getTime() - lastActive;
|
||||
// Don't refresh if they are still active in the window
|
||||
doRefresh = diffSeconds >= 5000;
|
||||
}
|
||||
const biometricLockedFingerprintValidated =
|
||||
(await this.stateService.getBiometricFingerprintValidated()) &&
|
||||
(await this.stateService.getBiometricLocked());
|
||||
if (doRefresh && !biometricLockedFingerprintValidated) {
|
||||
clearInterval(this.reloadInterval);
|
||||
this.reloadInterval = null;
|
||||
this.messagingService.send("reloadProcess");
|
||||
if (this.reloadCallback != null) {
|
||||
await this.reloadCallback();
|
||||
}
|
||||
}
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
cancelProcessReload(): void {
|
||||
if (this.reloadInterval != null) {
|
||||
clearInterval(this.reloadInterval);
|
||||
this.reloadInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
async clearClipboard(clipboardValue: string, timeoutMs: number = null): Promise<void> {
|
||||
if (this.clearClipboardTimeout != null) {
|
||||
clearTimeout(this.clearClipboardTimeout);
|
||||
this.clearClipboardTimeout = null;
|
||||
}
|
||||
if (Utils.isNullOrWhitespace(clipboardValue)) {
|
||||
return;
|
||||
}
|
||||
await this.stateService.getClearClipboard().then((clearSeconds) => {
|
||||
if (clearSeconds == null) {
|
||||
return;
|
||||
}
|
||||
if (timeoutMs == null) {
|
||||
timeoutMs = clearSeconds * 1000;
|
||||
}
|
||||
this.clearClipboardTimeoutFunction = async () => {
|
||||
const clipboardValueNow = await this.platformUtilsService.readFromClipboard();
|
||||
if (clipboardValue === clipboardValueNow) {
|
||||
this.platformUtilsService.copyToClipboard("", { clearing: true });
|
||||
}
|
||||
};
|
||||
this.clearClipboardTimeout = setTimeout(async () => {
|
||||
await this.clearPendingClipboard();
|
||||
}, timeoutMs);
|
||||
});
|
||||
}
|
||||
|
||||
async clearPendingClipboard() {
|
||||
if (this.clearClipboardTimeoutFunction != null) {
|
||||
await this.clearClipboardTimeoutFunction();
|
||||
this.clearClipboardTimeoutFunction = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
import { ApiService } from "../abstractions/api.service";
|
||||
import { CryptoService } from "../abstractions/crypto.service";
|
||||
import { I18nService } from "../abstractions/i18n.service";
|
||||
import { UserVerificationService as UserVerificationServiceAbstraction } from "../abstractions/userVerification.service";
|
||||
import { VerificationType } from "../enums/verificationType";
|
||||
import { VerifyOTPRequest } from "../models/request/account/verifyOTPRequest";
|
||||
import { SecretVerificationRequest } from "../models/request/secretVerificationRequest";
|
||||
import { Verification } from "../types/verification";
|
||||
|
||||
/**
|
||||
* Used for general-purpose user verification throughout the app.
|
||||
* Use it to verify the input collected by UserVerificationComponent.
|
||||
*/
|
||||
export class UserVerificationService implements UserVerificationServiceAbstraction {
|
||||
constructor(
|
||||
private cryptoService: CryptoService,
|
||||
private i18nService: I18nService,
|
||||
private apiService: ApiService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Create a new request model to be used for server-side verification
|
||||
* @param verification User-supplied verification data (Master Password or OTP)
|
||||
* @param requestClass The request model to create
|
||||
* @param alreadyHashed Whether the master password is already hashed
|
||||
*/
|
||||
async buildRequest<T extends SecretVerificationRequest>(
|
||||
verification: Verification,
|
||||
requestClass?: new () => T,
|
||||
alreadyHashed?: boolean,
|
||||
) {
|
||||
this.validateInput(verification);
|
||||
|
||||
const request =
|
||||
requestClass != null ? new requestClass() : (new SecretVerificationRequest() as T);
|
||||
|
||||
if (verification.type === VerificationType.OTP) {
|
||||
request.otp = verification.secret;
|
||||
} else {
|
||||
request.masterPasswordHash = alreadyHashed
|
||||
? verification.secret
|
||||
: await this.cryptoService.hashPassword(verification.secret, null);
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to verify the Master Password client-side, or send the OTP to the server for verification (with no other data)
|
||||
* Generally used for client-side verification only.
|
||||
* @param verification User-supplied verification data (Master Password or OTP)
|
||||
*/
|
||||
async verifyUser(verification: Verification): Promise<boolean> {
|
||||
this.validateInput(verification);
|
||||
|
||||
if (verification.type === VerificationType.OTP) {
|
||||
const request = new VerifyOTPRequest(verification.secret);
|
||||
try {
|
||||
await this.apiService.postAccountVerifyOTP(request);
|
||||
} catch (e) {
|
||||
throw new Error(this.i18nService.t("invalidVerificationCode"));
|
||||
}
|
||||
} else {
|
||||
const passwordValid = await this.cryptoService.compareAndUpdateKeyHash(
|
||||
verification.secret,
|
||||
null,
|
||||
);
|
||||
if (!passwordValid) {
|
||||
throw new Error(this.i18nService.t("invalidMasterPassword"));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async requestOTP() {
|
||||
await this.apiService.postAccountRequestOTP();
|
||||
}
|
||||
|
||||
private validateInput(verification: Verification) {
|
||||
if (verification?.secret == null || verification.secret === "") {
|
||||
if (verification.type === VerificationType.OTP) {
|
||||
throw new Error(this.i18nService.t("verificationCodeRequired"));
|
||||
} else {
|
||||
throw new Error(this.i18nService.t("masterPassRequired"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
import { CryptoFunctionService } from "@/jslib/common/src/abstractions/cryptoFunction.service";
|
||||
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
|
||||
import { StateService } from "@/jslib/common/src/abstractions/state.service";
|
||||
import { KeySuffixOptions } from "@/jslib/common/src/enums/keySuffixOptions";
|
||||
import { SymmetricCryptoKey } from "@/jslib/common/src/models/domain/symmetricCryptoKey";
|
||||
import { CryptoService } from "@/jslib/common/src/services/crypto.service";
|
||||
|
||||
export class ElectronCryptoService extends CryptoService {
|
||||
constructor(
|
||||
cryptoFunctionService: CryptoFunctionService,
|
||||
platformUtilService: PlatformUtilsService,
|
||||
logService: LogService,
|
||||
stateService: StateService,
|
||||
) {
|
||||
super(cryptoFunctionService, platformUtilService, logService, stateService);
|
||||
}
|
||||
|
||||
async hasKeyStored(keySuffix: KeySuffixOptions): Promise<boolean> {
|
||||
await this.upgradeSecurelyStoredKey();
|
||||
return super.hasKeyStored(keySuffix);
|
||||
}
|
||||
|
||||
protected async storeKey(key: SymmetricCryptoKey, userId?: string) {
|
||||
if (await this.shouldStoreKey(KeySuffixOptions.Auto, userId)) {
|
||||
await this.stateService.setCryptoMasterKeyAuto(key.keyB64, { userId: userId });
|
||||
} else {
|
||||
this.clearStoredKey(KeySuffixOptions.Auto);
|
||||
}
|
||||
|
||||
if (await this.shouldStoreKey(KeySuffixOptions.Biometric, userId)) {
|
||||
await this.stateService.setCryptoMasterKeyBiometric(key.keyB64, { userId: userId });
|
||||
} else {
|
||||
this.clearStoredKey(KeySuffixOptions.Biometric);
|
||||
}
|
||||
}
|
||||
|
||||
protected async retrieveKeyFromStorage(keySuffix: KeySuffixOptions, userId?: string) {
|
||||
await this.upgradeSecurelyStoredKey();
|
||||
return super.retrieveKeyFromStorage(keySuffix, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 4 Jun 2021 This is temporary upgrade method to move from a single shared stored key to
|
||||
* multiple, unique stored keys for each use, e.g. never logout vs. biometric authentication.
|
||||
*/
|
||||
private async upgradeSecurelyStoredKey() {
|
||||
// attempt key upgrade, but if we fail just delete it. Keys will be stored property upon unlock anyway.
|
||||
const key = await this.stateService.getCryptoMasterKeyB64();
|
||||
|
||||
if (key == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (await this.shouldStoreKey(KeySuffixOptions.Auto)) {
|
||||
await this.stateService.setCryptoMasterKeyAuto(key);
|
||||
}
|
||||
if (await this.shouldStoreKey(KeySuffixOptions.Biometric)) {
|
||||
await this.stateService.setCryptoMasterKeyBiometric(key);
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.error(
|
||||
`Encountered error while upgrading obsolete Bitwarden secure storage item:`,
|
||||
);
|
||||
this.logService.error(e);
|
||||
}
|
||||
|
||||
await this.stateService.setCryptoMasterKeyB64(null);
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ export class ElectronLogService extends BaseLogService {
|
||||
super(isDev(), filter);
|
||||
}
|
||||
|
||||
init () {
|
||||
init() {
|
||||
if (log.transports == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { EnvironmentService } from "@/jslib/common/src/abstractions/environment.
|
||||
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
|
||||
import { PasswordGenerationService } from "@/jslib/common/src/abstractions/passwordGeneration.service";
|
||||
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
|
||||
import { PolicyService } from "@/jslib/common/src/abstractions/policy.service";
|
||||
import { StateService } from "@/jslib/common/src/abstractions/state.service";
|
||||
import { TwoFactorService } from "@/jslib/common/src/abstractions/twoFactor.service";
|
||||
import { TwoFactorProviderType } from "@/jslib/common/src/enums/twoFactorProviderType";
|
||||
@@ -52,6 +53,7 @@ export class LoginCommand {
|
||||
protected platformUtilsService: PlatformUtilsService,
|
||||
protected stateService: StateService,
|
||||
protected cryptoService: CryptoService,
|
||||
protected policyService: PolicyService,
|
||||
protected twoFactorService: TwoFactorService,
|
||||
clientId: string,
|
||||
) {
|
||||
@@ -370,9 +372,23 @@ export class LoginCommand {
|
||||
const masterPasswordHint = hint.input;
|
||||
|
||||
// Retrieve details for key generation
|
||||
const enforcedPolicyOptions = await this.policyService.getMasterPasswordPolicyOptions();
|
||||
const kdf = await this.stateService.getKdfType();
|
||||
const kdfIterations = await this.stateService.getKdfIterations();
|
||||
|
||||
if (
|
||||
enforcedPolicyOptions != null &&
|
||||
!this.policyService.evaluateMasterPassword(
|
||||
strengthResult.score,
|
||||
masterPassword,
|
||||
enforcedPolicyOptions,
|
||||
)
|
||||
) {
|
||||
return this.updateTempPassword(
|
||||
"Your new master password does not meet the policy requirements.\n",
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// Create new key and hash new password
|
||||
const newKey = await this.cryptoService.makeKey(
|
||||
|
||||
630
package-lock.json
generated
630
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
19
package.json
19
package.json
@@ -2,7 +2,7 @@
|
||||
"name": "@bitwarden/directory-connector",
|
||||
"productName": "Bitwarden Directory Connector",
|
||||
"description": "Sync your user directory to your Bitwarden organization.",
|
||||
"version": "2024.7.0",
|
||||
"version": "2024.9.0",
|
||||
"keywords": [
|
||||
"bitwarden",
|
||||
"password",
|
||||
@@ -80,7 +80,6 @@
|
||||
"@types/jest": "29.5.11",
|
||||
"@types/ldapjs": "2.2.5",
|
||||
"@types/lowdb": "1.0.15",
|
||||
"@types/lunr": "2.3.7",
|
||||
"@types/node": "18.17.12",
|
||||
"@types/node-fetch": "2.6.10",
|
||||
"@types/node-forge": "1.3.11",
|
||||
@@ -114,14 +113,15 @@
|
||||
"html-webpack-plugin": "5.6.0",
|
||||
"husky": "9.0.10",
|
||||
"jest": "29.7.0",
|
||||
"jest-junit": "16.0.0",
|
||||
"jest-preset-angular": "13.1.1",
|
||||
"lint-staged": "15.2.0",
|
||||
"lint-staged": "15.2.10",
|
||||
"mini-css-extract-plugin": "2.7.7",
|
||||
"node-forge": "1.3.1",
|
||||
"node-loader": "2.0.0",
|
||||
"pkg": "5.8.1",
|
||||
"prettier": "3.2.2",
|
||||
"rimraf": "5.0.5",
|
||||
"prettier": "3.3.3",
|
||||
"rimraf": "5.0.10",
|
||||
"rxjs": "7.8.1",
|
||||
"sass": "1.69.7",
|
||||
"sass-loader": "14.0.0",
|
||||
@@ -130,9 +130,9 @@
|
||||
"tsconfig-paths-webpack-plugin": "4.1.0",
|
||||
"typescript": "4.9.5",
|
||||
"typescript-transform-paths": "3.4.6",
|
||||
"webpack": "5.89.0",
|
||||
"webpack": "5.94.0",
|
||||
"webpack-cli": "5.1.4",
|
||||
"webpack-merge": "5.10.0",
|
||||
"webpack-merge": "6.0.1",
|
||||
"webpack-node-externals": "3.0.0",
|
||||
"zone.js": "0.13.1"
|
||||
},
|
||||
@@ -147,8 +147,6 @@
|
||||
"@angular/platform-browser-dynamic": "16.2.12",
|
||||
"@angular/router": "16.2.12",
|
||||
"@microsoft/microsoft-graph-client": "3.0.7",
|
||||
"@microsoft/signalr": "7.0.10",
|
||||
"@microsoft/signalr-protocol-msgpack": "7.0.10",
|
||||
"big-integer": "1.6.52",
|
||||
"bootstrap": "4.6.2",
|
||||
"browser-hrtime": "1.1.8",
|
||||
@@ -163,7 +161,6 @@
|
||||
"keytar": "7.9.0",
|
||||
"ldapjs": "2.3.3",
|
||||
"lowdb": "1.0.0",
|
||||
"lunr": "2.3.9",
|
||||
"ngx-toastr": "16.2.0",
|
||||
"node-fetch": "2.7.0",
|
||||
"open": "8.4.2",
|
||||
@@ -175,7 +172,7 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": "~18",
|
||||
"npm": "~10.4.0"
|
||||
"npm": "~10"
|
||||
},
|
||||
"lint-staged": {
|
||||
"./!(jslib)**": "prettier --ignore-unknown --write",
|
||||
|
||||
@@ -7,15 +7,30 @@ exports.default = async function notarizing(context) {
|
||||
if (electronPlatformName !== "darwin") {
|
||||
return;
|
||||
}
|
||||
const appleId = process.env.APPLE_ID_USERNAME || process.env.APPLEID;
|
||||
const appleIdPassword = process.env.APPLE_ID_PASSWORD || `@keychain:AC_PASSWORD`;
|
||||
|
||||
const appName = context.packager.appInfo.productFilename;
|
||||
return await notarize({
|
||||
tool: "notarytool",
|
||||
appBundleId: "com.bitwarden.directory-connector",
|
||||
appPath: `${appOutDir}/${appName}.app`,
|
||||
teamId: "LTZ2PFU5D6",
|
||||
appleId: appleId,
|
||||
appleIdPassword: appleIdPassword,
|
||||
});
|
||||
if (process.env.APP_STORE_CONNECT_TEAM_ISSUER) {
|
||||
const appleApiIssuer = process.env.APP_STORE_CONNECT_TEAM_ISSUER;
|
||||
const appleApiKey = process.env.APP_STORE_CONNECT_AUTH_KEY_PATH;
|
||||
const appleApiKeyId = process.env.APP_STORE_CONNECT_AUTH_KEY;
|
||||
return await notarize({
|
||||
tool: "notarytool",
|
||||
appBundleId: "com.bitwarden.directory-connector",
|
||||
appPath: `${appOutDir}/${appName}.app`,
|
||||
appleApiIssuer: appleApiIssuer,
|
||||
appleApiKey: appleApiKey,
|
||||
appleApiKeyId: appleApiKeyId,
|
||||
});
|
||||
} else {
|
||||
const appleId = process.env.APPLE_ID_USERNAME || process.env.APPLEID;
|
||||
const appleIdPassword = process.env.APPLE_ID_PASSWORD || `@keychain:AC_PASSWORD`;
|
||||
return await notarize({
|
||||
tool: "notarytool",
|
||||
appBundleId: "com.bitwarden.directory-connector",
|
||||
appPath: `${appOutDir}/${appName}.app`,
|
||||
teamId: "LTZ2PFU5D6",
|
||||
appleId: appleId,
|
||||
appleIdPassword: appleIdPassword,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ import { CryptoService as CryptoServiceAbstraction } from "@/jslib/common/src/ab
|
||||
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@/jslib/common/src/abstractions/cryptoFunction.service";
|
||||
import { EnvironmentService as EnvironmentServiceAbstraction } from "@/jslib/common/src/abstractions/environment.service";
|
||||
import { I18nService as I18nServiceAbstraction } from "@/jslib/common/src/abstractions/i18n.service";
|
||||
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@/jslib/common/src/abstractions/keyConnector.service";
|
||||
import { LogService as LogServiceAbstraction } from "@/jslib/common/src/abstractions/log.service";
|
||||
import { MessagingService as MessagingServiceAbstraction } from "@/jslib/common/src/abstractions/messaging.service";
|
||||
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@/jslib/common/src/abstractions/platformUtils.service";
|
||||
@@ -152,6 +153,7 @@ export function initFactory(
|
||||
PlatformUtilsServiceAbstraction,
|
||||
MessagingServiceAbstraction,
|
||||
LogServiceAbstraction,
|
||||
KeyConnectorServiceAbstraction,
|
||||
EnvironmentServiceAbstraction,
|
||||
StateServiceAbstraction,
|
||||
TwoFactorServiceAbstraction,
|
||||
|
||||
@@ -105,6 +105,11 @@
|
||||
<li *ngFor="let g of simGroups" title="{{ g.referenceId }}">
|
||||
<i class="bwi bwi-li bwi-sitemap"></i>
|
||||
{{ g.displayName }}
|
||||
<ul class="small" *ngIf="g.users && g.users.length">
|
||||
<li *ngFor="let u of g.users" title="{{ u.referenceId }}">
|
||||
{{ u.displayName }}
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<p *ngIf="!simGroups || !simGroups.length">{{ "noGroups" | i18n }}</p>
|
||||
|
||||
@@ -419,7 +419,7 @@
|
||||
name="AdminUser"
|
||||
[(ngModel)]="gsuite.adminUser"
|
||||
/>
|
||||
<small class="text-muted form-text">{{ "ex" | i18n }} admin@company.com</small>
|
||||
<small class="text-muted form-text">{{ "ex" | i18n }} admin@company.com</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="customerId">{{ "customerId" | i18n }}</label>
|
||||
@@ -596,7 +596,7 @@
|
||||
name="EmailSuffix"
|
||||
[(ngModel)]="sync.emailSuffix"
|
||||
/>
|
||||
<small class="text-muted form-text">{{ "ex" | i18n }} @company.com</small>
|
||||
<small class="text-muted form-text">{{ "ex" | i18n }} @company.com</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -628,13 +628,13 @@
|
||||
<small
|
||||
class="text-muted form-text"
|
||||
*ngIf="directory === directoryType.AzureActiveDirectory"
|
||||
>{{ "ex" | i18n }} exclude:joe@company.com</small
|
||||
>{{ "ex" | i18n }} exclude:joe@company.com</small
|
||||
>
|
||||
<small class="text-muted form-text" *ngIf="directory === directoryType.Okta"
|
||||
>{{ "ex" | i18n }} exclude:joe@company.com | profile.firstName eq "John"</small
|
||||
>{{ "ex" | i18n }} exclude:joe@company.com | profile.firstName eq "John"</small
|
||||
>
|
||||
<small class="text-muted form-text" *ngIf="directory === directoryType.GSuite"
|
||||
>{{ "ex" | i18n }} exclude:joe@company.com | orgName=Engineering</small
|
||||
>{{ "ex" | i18n }} exclude:joe@company.com | orgName=Engineering</small
|
||||
>
|
||||
</div>
|
||||
<div class="form-group" [hidden]="directory != directoryType.Ldap">
|
||||
|
||||
20
src/bwdc.ts
20
src/bwdc.ts
@@ -15,8 +15,7 @@ import { KeyConnectorService } from "@/jslib/common/src/services/keyConnector.se
|
||||
import { NoopMessagingService } from "@/jslib/common/src/services/noopMessaging.service";
|
||||
import { OrganizationService } from "@/jslib/common/src/services/organization.service";
|
||||
import { PasswordGenerationService } from "@/jslib/common/src/services/passwordGeneration.service";
|
||||
import { SearchService } from "@/jslib/common/src/services/search.service";
|
||||
import { SettingsService } from "@/jslib/common/src/services/settings.service";
|
||||
import { PolicyService } from "@/jslib/common/src/services/policy.service";
|
||||
import { TokenService } from "@/jslib/common/src/services/token.service";
|
||||
import { CliPlatformUtilsService } from "@/jslib/node/src/cli/services/cliPlatformUtils.service";
|
||||
import { ConsoleLogService } from "@/jslib/node/src/cli/services/consoleLog.service";
|
||||
@@ -37,7 +36,6 @@ import { SyncService } from "./services/sync.service";
|
||||
// eslint-disable-next-line
|
||||
const packageJson = require("../package.json");
|
||||
|
||||
export const searchService: SearchService = null;
|
||||
export class Main {
|
||||
dataFilePath: string;
|
||||
logService: ConsoleLogService;
|
||||
@@ -54,10 +52,9 @@ export class Main {
|
||||
containerService: ContainerService;
|
||||
cryptoFunctionService: NodeCryptoFunctionService;
|
||||
authService: AuthService;
|
||||
searchService: SearchService;
|
||||
settingsService: SettingsService;
|
||||
syncService: SyncService;
|
||||
passwordGenerationService: PasswordGenerationService;
|
||||
policyService: PolicyService;
|
||||
keyConnectorService: KeyConnectorService;
|
||||
program: Program;
|
||||
stateService: StateService;
|
||||
@@ -190,14 +187,17 @@ export class Main {
|
||||
this.stateService,
|
||||
);
|
||||
|
||||
this.passwordGenerationService = new PasswordGenerationService(
|
||||
this.cryptoService,
|
||||
this.policyService = new PolicyService(
|
||||
this.stateService,
|
||||
this.organizationService,
|
||||
this.apiService,
|
||||
);
|
||||
|
||||
this.settingsService = new SettingsService(this.stateService);
|
||||
|
||||
this.searchService = new SearchService(this.logService, this.i18nService);
|
||||
this.passwordGenerationService = new PasswordGenerationService(
|
||||
this.cryptoService,
|
||||
this.policyService,
|
||||
this.stateService,
|
||||
);
|
||||
|
||||
this.program = new Program(this);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Entry } from "./entry";
|
||||
import { UserEntry } from "./userEntry";
|
||||
|
||||
export class GroupEntry extends Entry {
|
||||
name: string;
|
||||
userMemberExternalIds = new Set<string>();
|
||||
groupMemberReferenceIds = new Set<string>();
|
||||
users: UserEntry[] = [];
|
||||
|
||||
get displayName(): string {
|
||||
if (this.name == null) {
|
||||
|
||||
@@ -102,6 +102,7 @@ export class Program extends BaseProgram {
|
||||
this.main.platformUtilsService,
|
||||
this.main.stateService,
|
||||
this.main.cryptoService,
|
||||
this.main.policyService,
|
||||
this.main.twoFactorService,
|
||||
"connector",
|
||||
);
|
||||
|
||||
@@ -199,8 +199,7 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements IDir
|
||||
const p = Object.assign({ groupKey: group.id, pageToken: nextPageToken }, this.authParams);
|
||||
const memRes = await this.service.members.list(p);
|
||||
if (memRes.status !== 200) {
|
||||
this.logService.warning("Group member list API failed: " + memRes.statusText);
|
||||
return entry;
|
||||
throw new Error("Group member list API failed: " + memRes.statusText);
|
||||
}
|
||||
|
||||
nextPageToken = memRes.data.nextPageToken;
|
||||
|
||||
@@ -49,7 +49,7 @@ export class LdapDirectoryService implements IDirectoryService {
|
||||
|
||||
let users: UserEntry[];
|
||||
if (this.syncConfig.users) {
|
||||
users = await this.getUsers(force);
|
||||
users = await this.getUsers(force, test);
|
||||
}
|
||||
|
||||
let groups: GroupEntry[];
|
||||
@@ -66,7 +66,7 @@ export class LdapDirectoryService implements IDirectoryService {
|
||||
return [groups, users];
|
||||
}
|
||||
|
||||
private async getUsers(force: boolean): Promise<UserEntry[]> {
|
||||
private async getUsers(force: boolean, test: boolean): Promise<UserEntry[]> {
|
||||
const lastSync = await this.stateService.getLastUserSync();
|
||||
let filter = this.buildBaseFilter(this.syncConfig.userObjectClass, this.syncConfig.userFilter);
|
||||
filter = this.buildRevisionFilter(filter, force, lastSync);
|
||||
@@ -77,7 +77,20 @@ export class LdapDirectoryService implements IDirectoryService {
|
||||
const regularUsers = await this.search<UserEntry>(path, filter, (se: any) =>
|
||||
this.buildUser(se, false),
|
||||
);
|
||||
if (!this.dirConfig.ad) {
|
||||
|
||||
// Active Directory has a special way of managing deleted users that
|
||||
// standard LDAP does not. Users can be "tombstoned", where they cease to
|
||||
// exist, or they can be "recycled" where they exist in a quarantined
|
||||
// state for a period of time before being tombstoned.
|
||||
//
|
||||
// Essentially, recycled users are soft deleted but tombstoned users are
|
||||
// hard deleted. In standard LDAP deleted users are only ever hard
|
||||
// deleted.
|
||||
//
|
||||
// We check for recycled Active Directory users below, but only if the
|
||||
// sync is a test sync or the "Overwrite existing users" flag is checked.
|
||||
const ignoreDeletedUsers = !this.dirConfig.ad || (!force && !test);
|
||||
if (ignoreDeletedUsers) {
|
||||
return regularUsers;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user