mirror of
https://github.com/bitwarden/directory-connector
synced 2025-12-16 08:14:01 +00:00
Compare commits
52 Commits
update-lda
...
v2.9.10
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4e9b6081a | ||
|
|
e107998586 | ||
|
|
6de48441f7 | ||
|
|
0de0b88aec | ||
|
|
d519c39761 | ||
|
|
a578fb49c7 | ||
|
|
1be64836f4 | ||
|
|
f2389189a3 | ||
|
|
bb4be6022b | ||
|
|
f85a0c5ea5 | ||
|
|
5afae04b1d | ||
|
|
d1b182d20b | ||
|
|
9e3d1caee4 | ||
|
|
9a78956b23 | ||
|
|
822655b944 | ||
|
|
6dfbe505d9 | ||
|
|
0809c2c104 | ||
|
|
e30000bd00 | ||
|
|
90a7601960 | ||
|
|
8a800c6d33 | ||
|
|
d0021c9306 | ||
|
|
97673c84da | ||
|
|
771a182235 | ||
|
|
857d725a77 | ||
|
|
25b3e0f691 | ||
|
|
d2ba7631b5 | ||
|
|
a893c78c74 | ||
|
|
5ff041aa7b | ||
|
|
096196fcd5 | ||
|
|
225073aa33 | ||
|
|
f8b26d82d8 | ||
|
|
6b98a46b94 | ||
|
|
13572b94ee | ||
|
|
999b790557 | ||
|
|
7c93d59a42 | ||
|
|
9bec2aa2f0 | ||
|
|
240e1d5813 | ||
|
|
d82f4d90c1 | ||
|
|
abc68e8ef9 | ||
|
|
660ee538ce | ||
|
|
a96144d6dc | ||
|
|
e43d192007 | ||
|
|
74a018edb8 | ||
|
|
07d0049183 | ||
|
|
5f5358ea0f | ||
|
|
36cc6552bf | ||
|
|
05b5fd2eb4 | ||
|
|
95f1e86509 | ||
|
|
378dd06274 | ||
|
|
314adeb164 | ||
|
|
cc4f8c9f8d | ||
|
|
35b0e81beb |
@@ -7,10 +7,9 @@ root = true
|
|||||||
[*]
|
[*]
|
||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
quote_type = single
|
|
||||||
|
|
||||||
# Set default charset
|
# Set default charset
|
||||||
[*.{js,ts,scss,html}]
|
[*.{js,ts,scss,html}]
|
||||||
charset = utf-8
|
charset = utf-8
|
||||||
indent_style = space
|
indent_style = space
|
||||||
indent_size = 4
|
indent_size = 2
|
||||||
|
|||||||
2
.git-blame-ignore-revs
Normal file
2
.git-blame-ignore-revs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Apply Prettier https://github.com/bitwarden/directory-connector/pull/194
|
||||||
|
096196fcd512944d1c3d9c007647a1319b032639
|
||||||
33
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
33
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
## Type of change
|
||||||
|
|
||||||
|
- [ ] Bug fix
|
||||||
|
- [ ] New feature development
|
||||||
|
- [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc)
|
||||||
|
- [ ] Build/deploy pipeline (DevOps)
|
||||||
|
- [ ] Other
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
<!--Describe what the purpose of this PR is. For example: what bug you're fixing or what new feature you're adding-->
|
||||||
|
|
||||||
|
## Code changes
|
||||||
|
|
||||||
|
<!--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-->
|
||||||
|
|
||||||
|
- **file.ext:** Description of what was changed and why
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
|
||||||
|
<!--Required for any UI changes. Delete if not applicable-->
|
||||||
|
|
||||||
|
## Testing requirements
|
||||||
|
|
||||||
|
<!--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)
|
||||||
68
.github/workflows/build.yml
vendored
68
.github/workflows/build.yml
vendored
@@ -46,7 +46,7 @@ jobs:
|
|||||||
needs: setup
|
needs: setup
|
||||||
env:
|
env:
|
||||||
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
|
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
|
||||||
_PKG_FETCH_NODE_VERSION: 14.17.6
|
_PKG_FETCH_NODE_VERSION: 16.13.0
|
||||||
_PKG_FETCH_VERSION: 3.2
|
_PKG_FETCH_VERSION: 3.2
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
@@ -55,11 +55,10 @@ jobs:
|
|||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
|
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
|
||||||
with:
|
with:
|
||||||
node-version: '14'
|
node-version: '16'
|
||||||
|
|
||||||
- name: Update NPM
|
- name: Update NPM
|
||||||
run: |
|
run: |
|
||||||
npm install -g npm@7
|
|
||||||
npm install -g node-gyp
|
npm install -g node-gyp
|
||||||
node-gyp install $(node -v)
|
node-gyp install $(node -v)
|
||||||
|
|
||||||
@@ -74,8 +73,7 @@ jobs:
|
|||||||
- name: Keytar
|
- name: Keytar
|
||||||
run: |
|
run: |
|
||||||
keytarVersion=$(cat src/package.json | jq -r '.dependencies.keytar')
|
keytarVersion=$(cat src/package.json | jq -r '.dependencies.keytar')
|
||||||
nodeModVersion=$(node -e "console.log(process.config.variables.node_module_version)")
|
keytarTar="keytar-v$keytarVersion-napi-v3-linux-x64.tar"
|
||||||
keytarTar="keytar-v$keytarVersion-node-v$nodeModVersion-linux-x64.tar"
|
|
||||||
|
|
||||||
keytarTarGz="$keytarTar.gz"
|
keytarTarGz="$keytarTar.gz"
|
||||||
keytarUrl="https://github.com/atom/node-keytar/releases/download/v$keytarVersion/$keytarTarGz"
|
keytarUrl="https://github.com/atom/node-keytar/releases/download/v$keytarVersion/$keytarTarGz"
|
||||||
@@ -100,7 +98,11 @@ jobs:
|
|||||||
|
|
||||||
- name: Version Test
|
- name: Version Test
|
||||||
run: |
|
run: |
|
||||||
sudo apt install libsecret-1-0
|
sudo apt install libsecret-1-0 dbus-x11 gnome-keyring
|
||||||
|
eval $(dbus-launch --sh-syntax)
|
||||||
|
|
||||||
|
eval $(echo -n "" | /usr/bin/gnome-keyring-daemon --login)
|
||||||
|
eval $(/usr/bin/gnome-keyring-daemon --components=secrets --start)
|
||||||
|
|
||||||
mkdir -p test/linux
|
mkdir -p test/linux
|
||||||
unzip ./dist-cli/bwdc-linux-$_PACKAGE_VERSION.zip -d ./test/linux
|
unzip ./dist-cli/bwdc-linux-$_PACKAGE_VERSION.zip -d ./test/linux
|
||||||
@@ -110,7 +112,7 @@ jobs:
|
|||||||
echo "version: $_PACKAGE_VERSION"
|
echo "version: $_PACKAGE_VERSION"
|
||||||
echo "testVersion: $testVersion"
|
echo "testVersion: $testVersion"
|
||||||
|
|
||||||
if [ "$testVersion" != "$_PACKAGE_VERSION" ]; then
|
if [ "$testVersion" != "$_PACKAGE_VERSION" ]; then
|
||||||
echo "Version test failed."
|
echo "Version test failed."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -136,7 +138,7 @@ jobs:
|
|||||||
needs: setup
|
needs: setup
|
||||||
env:
|
env:
|
||||||
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
|
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
|
||||||
_PKG_FETCH_NODE_VERSION: 14.17.6
|
_PKG_FETCH_NODE_VERSION: 16.13.0
|
||||||
_PKG_FETCH_VERSION: 3.2
|
_PKG_FETCH_VERSION: 3.2
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
@@ -145,11 +147,10 @@ jobs:
|
|||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
|
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
|
||||||
with:
|
with:
|
||||||
node-version: '14'
|
node-version: '16'
|
||||||
|
|
||||||
- name: Update NPM
|
- name: Update NPM
|
||||||
run: |
|
run: |
|
||||||
npm install -g npm@7
|
|
||||||
npm install -g node-gyp
|
npm install -g node-gyp
|
||||||
node-gyp install $(node -v)
|
node-gyp install $(node -v)
|
||||||
|
|
||||||
@@ -164,8 +165,7 @@ jobs:
|
|||||||
- name: Keytar
|
- name: Keytar
|
||||||
run: |
|
run: |
|
||||||
keytarVersion=$(cat src/package.json | jq -r '.dependencies.keytar')
|
keytarVersion=$(cat src/package.json | jq -r '.dependencies.keytar')
|
||||||
nodeModVersion=$(node -e "console.log(process.config.variables.node_module_version)")
|
keytarTar="keytar-v$keytarVersion-napi-v3-darwin-x64.tar"
|
||||||
keytarTar="keytar-v$keytarVersion-node-v$nodeModVersion-darwin-x64.tar"
|
|
||||||
|
|
||||||
keytarTarGz="$keytarTar.gz"
|
keytarTarGz="$keytarTar.gz"
|
||||||
keytarUrl="https://github.com/atom/node-keytar/releases/download/v$keytarVersion/$keytarTarGz"
|
keytarUrl="https://github.com/atom/node-keytar/releases/download/v$keytarVersion/$keytarTarGz"
|
||||||
@@ -198,7 +198,7 @@ jobs:
|
|||||||
echo "version: $_PACKAGE_VERSION"
|
echo "version: $_PACKAGE_VERSION"
|
||||||
echo "testVersion: $testVersion"
|
echo "testVersion: $testVersion"
|
||||||
|
|
||||||
if [ "$testVersion" != "$_PACKAGE_VERSION" ]; then
|
if [ "$testVersion" != "$_PACKAGE_VERSION" ]; then
|
||||||
echo "Version test failed."
|
echo "Version test failed."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -224,7 +224,7 @@ jobs:
|
|||||||
needs: setup
|
needs: setup
|
||||||
env:
|
env:
|
||||||
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
|
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
|
||||||
_WIN_PKG_FETCH_VERSION: 14.17.6
|
_WIN_PKG_FETCH_VERSION: 16.13.0
|
||||||
_WIN_PKG_VERSION: 3.2
|
_WIN_PKG_VERSION: 3.2
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
@@ -238,11 +238,10 @@ jobs:
|
|||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
|
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
|
||||||
with:
|
with:
|
||||||
node-version: '14'
|
node-version: '16'
|
||||||
|
|
||||||
- name: Update NPM
|
- name: Update NPM
|
||||||
run: |
|
run: |
|
||||||
npm install -g npm@7
|
|
||||||
npm install -g node-gyp
|
npm install -g node-gyp
|
||||||
node-gyp install $(node -v)
|
node-gyp install $(node -v)
|
||||||
|
|
||||||
@@ -261,8 +260,7 @@ jobs:
|
|||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: |
|
run: |
|
||||||
$keytarVersion = (Get-Content -Raw -Path ./src/package.json | ConvertFrom-Json).dependencies.keytar
|
$keytarVersion = (Get-Content -Raw -Path ./src/package.json | ConvertFrom-Json).dependencies.keytar
|
||||||
$nodeModVersion = node -e "console.log(process.config.variables.node_module_version)"
|
$keytarTar = "keytar-v${keytarVersion}-napi-v3-{0}-x64.tar"
|
||||||
$keytarTar = "keytar-v${keytarVersion}-node-v${nodeModVersion}-{0}-x64.tar"
|
|
||||||
$keytarTarGz = "${keytarTar}.gz"
|
$keytarTarGz = "${keytarTar}.gz"
|
||||||
$keytarUrl = "https://github.com/atom/node-keytar/releases/download/v${keytarVersion}/${keytarTarGz}"
|
$keytarUrl = "https://github.com/atom/node-keytar/releases/download/v${keytarVersion}/${keytarTarGz}"
|
||||||
|
|
||||||
@@ -378,11 +376,10 @@ jobs:
|
|||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
|
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
|
||||||
with:
|
with:
|
||||||
node-version: '14'
|
node-version: '16'
|
||||||
|
|
||||||
- name: Update NPM
|
- name: Update NPM
|
||||||
run: |
|
run: |
|
||||||
npm install -g npm@7
|
|
||||||
npm install -g node-gyp
|
npm install -g node-gyp
|
||||||
node-gyp install $(node -v)
|
node-gyp install $(node -v)
|
||||||
|
|
||||||
@@ -405,8 +402,8 @@ jobs:
|
|||||||
- name: Install Node dependencies
|
- name: Install Node dependencies
|
||||||
run: npm install
|
run: npm install
|
||||||
|
|
||||||
- name: Run linter
|
# - name: Run linter
|
||||||
run: npm run lint
|
# run: npm run lint
|
||||||
|
|
||||||
- name: Build & Sign
|
- name: Build & Sign
|
||||||
run: npm run dist:win
|
run: npm run dist:win
|
||||||
@@ -418,9 +415,6 @@ jobs:
|
|||||||
SIGNING_CLIENT_SECRET: ${{ secrets.SIGNING_CLIENT_SECRET }}
|
SIGNING_CLIENT_SECRET: ${{ secrets.SIGNING_CLIENT_SECRET }}
|
||||||
SIGNING_CERT_NAME: ${{ secrets.SIGNING_CERT_NAME }}
|
SIGNING_CERT_NAME: ${{ secrets.SIGNING_CERT_NAME }}
|
||||||
|
|
||||||
- name: List Dist
|
|
||||||
run: dir ./dist
|
|
||||||
|
|
||||||
- name: Upload Portable Executable to GitHub
|
- name: Upload Portable Executable to GitHub
|
||||||
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
|
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
|
||||||
with:
|
with:
|
||||||
@@ -435,6 +429,13 @@ jobs:
|
|||||||
path: ./dist/Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe
|
path: ./dist/Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
|
- name: Upload Installer Executable Blockmap to GitHub
|
||||||
|
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
|
||||||
|
with:
|
||||||
|
name: Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe.blockmap
|
||||||
|
path: ./dist/Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe.blockmap
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload latest auto-update artifact
|
- name: Upload latest auto-update artifact
|
||||||
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
|
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
|
||||||
with:
|
with:
|
||||||
@@ -453,11 +454,10 @@ jobs:
|
|||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
|
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
|
||||||
with:
|
with:
|
||||||
node-version: '14'
|
node-version: '16'
|
||||||
|
|
||||||
- name: Update NPM
|
- name: Update NPM
|
||||||
run: |
|
run: |
|
||||||
npm install -g npm@7
|
|
||||||
npm install -g node-gyp
|
npm install -g node-gyp
|
||||||
node-gyp install $(node -v)
|
node-gyp install $(node -v)
|
||||||
|
|
||||||
@@ -507,11 +507,10 @@ jobs:
|
|||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
|
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
|
||||||
with:
|
with:
|
||||||
node-version: '14'
|
node-version: '16'
|
||||||
|
|
||||||
- name: Update NPM
|
- name: Update NPM
|
||||||
run: |
|
run: |
|
||||||
npm install -g npm@7
|
|
||||||
npm install -g node-gyp
|
npm install -g node-gyp
|
||||||
node-gyp install $(node -v)
|
node-gyp install $(node -v)
|
||||||
|
|
||||||
@@ -579,8 +578,8 @@ jobs:
|
|||||||
- name: Install Node dependencies
|
- name: Install Node dependencies
|
||||||
run: npm install
|
run: npm install
|
||||||
|
|
||||||
- name: Run linter
|
# - name: Run linter
|
||||||
run: npm run lint
|
# run: npm run lint
|
||||||
|
|
||||||
- name: Build application
|
- name: Build application
|
||||||
run: npm run dist:mac
|
run: npm run dist:mac
|
||||||
@@ -608,6 +607,13 @@ jobs:
|
|||||||
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg
|
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
|
- name: Upload .dmg Blockmap artifact
|
||||||
|
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
|
||||||
|
with:
|
||||||
|
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg.blockmap
|
||||||
|
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg.blockmap
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload latest auto-update artifact
|
- name: Upload latest auto-update artifact
|
||||||
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
|
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
|
||||||
with:
|
with:
|
||||||
|
|||||||
26
.github/workflows/release.yml
vendored
26
.github/workflows/release.yml
vendored
@@ -3,6 +3,15 @@ name: Release
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
release_type:
|
||||||
|
description: 'Release Options'
|
||||||
|
required: true
|
||||||
|
default: 'Initial Release'
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- Initial Release
|
||||||
|
- Redeploy
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
setup:
|
setup:
|
||||||
@@ -11,17 +20,15 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Branch check
|
- name: Branch check
|
||||||
run: |
|
run: |
|
||||||
if [[ "$GITHUB_REF" != "refs/heads/release" ]]; then
|
if [[ "$GITHUB_REF" != "refs/heads/rc" ]] && [[ "$GITHUB_REF" != "refs/heads/hotfix" ]]; then
|
||||||
echo "==================================="
|
echo "==================================="
|
||||||
echo "[!] Can only release from the 'release' branch"
|
echo "[!] Can only release from the 'rc' or 'hotfix' branches"
|
||||||
echo "==================================="
|
echo "==================================="
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
||||||
with:
|
|
||||||
ref: release
|
|
||||||
|
|
||||||
- name: Retrieve Directory Connector release version
|
- name: Retrieve Directory Connector release version
|
||||||
id: retrieve-version
|
id: retrieve-version
|
||||||
@@ -30,6 +37,7 @@ jobs:
|
|||||||
echo "::set-output name=package_version::$PKG_VERSION"
|
echo "::set-output name=package_version::$PKG_VERSION"
|
||||||
|
|
||||||
- name: Check to make sure Mobile release version has been bumped
|
- name: Check to make sure Mobile release version has been bumped
|
||||||
|
if: ${{ github.event.inputs.release_type == 'Initial Release' }}
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
@@ -44,12 +52,18 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
|
- name: Get branch name
|
||||||
|
id: branch
|
||||||
|
run: |
|
||||||
|
BRANCH_NAME=$(basename ${{ github.ref }})
|
||||||
|
echo "::set-output name=branch-name::$BRANCH_NAME"
|
||||||
|
|
||||||
- name: Download all artifacts
|
- name: Download all artifacts
|
||||||
uses: bitwarden/gh-actions/download-artifacts@23433be15ed6fd046ce12b6889c5184a8d9c8783
|
uses: bitwarden/gh-actions/download-artifacts@23433be15ed6fd046ce12b6889c5184a8d9c8783
|
||||||
with:
|
with:
|
||||||
workflow: build.yml
|
workflow: build.yml
|
||||||
workflow_conclusion: success
|
workflow_conclusion: success
|
||||||
branch: release
|
branch: ${{ steps.branch.outputs.branch-name }}
|
||||||
|
|
||||||
- name: Create release
|
- name: Create release
|
||||||
uses: ncipollo/release-action@95215a3cb6e6a1908b3c44e00b4fdb15548b1e09 # v2.8.5
|
uses: ncipollo/release-action@95215a3cb6e6a1908b3c44e00b4fdb15548b1e09 # v2.8.5
|
||||||
@@ -64,9 +78,11 @@ jobs:
|
|||||||
./bwdc-linux-sha256-${{ env.PKG_VERSION }}.txt,
|
./bwdc-linux-sha256-${{ env.PKG_VERSION }}.txt,
|
||||||
./Bitwarden-Connector-Portable-${{ env.PKG_VERSION }}.exe,
|
./Bitwarden-Connector-Portable-${{ env.PKG_VERSION }}.exe,
|
||||||
./Bitwarden-Connector-Installer-${{ env.PKG_VERSION }}.exe,
|
./Bitwarden-Connector-Installer-${{ env.PKG_VERSION }}.exe,
|
||||||
|
./Bitwarden-Connector-Installer-${{ env.PKG_VERSION }}.exe.blockmap,
|
||||||
./Bitwarden-Connector-${{ env.PKG_VERSION }}-x86_64.AppImage,
|
./Bitwarden-Connector-${{ env.PKG_VERSION }}-x86_64.AppImage,
|
||||||
./Bitwarden-Connector-${{ env.PKG_VERSION }}-mac.zip,
|
./Bitwarden-Connector-${{ env.PKG_VERSION }}-mac.zip,
|
||||||
./Bitwarden-Connector-${{ env.PKG_VERSION }}.dmg,
|
./Bitwarden-Connector-${{ env.PKG_VERSION }}.dmg,
|
||||||
|
./Bitwarden-Connector-${{ env.PKG_VERSION }}.dmg.blockmap,
|
||||||
./latest-linux.yml,
|
./latest-linux.yml,
|
||||||
./latest-mac.yml,
|
./latest-mac.yml,
|
||||||
./latest.yml"
|
./latest.yml"
|
||||||
|
|||||||
65
.github/workflows/version-bump.yml
vendored
Normal file
65
.github/workflows/version-bump.yml
vendored
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
---
|
||||||
|
name: Version Bump
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version_number:
|
||||||
|
description: "New Version"
|
||||||
|
required: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
bump_version:
|
||||||
|
name: "Create version_bump_${{ github.event.inputs.version_number }} branch"
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
steps:
|
||||||
|
- name: Checkout Branch
|
||||||
|
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
|
||||||
|
|
||||||
|
- name: Create Version Branch
|
||||||
|
run: |
|
||||||
|
git switch -c version_bump_${{ github.event.inputs.version_number }}
|
||||||
|
git push -u origin version_bump_${{ github.event.inputs.version_number }}
|
||||||
|
|
||||||
|
- name: Checkout Version Branch
|
||||||
|
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
|
||||||
|
with:
|
||||||
|
ref: version_bump_${{ github.event.inputs.version_number }}
|
||||||
|
|
||||||
|
- name: Bump Version - Package
|
||||||
|
uses: bitwarden/gh-actions/version-bump@03ad9a873c39cdc95dd8d77dbbda67f84db43945
|
||||||
|
with:
|
||||||
|
version: ${{ github.event.inputs.version_number }}
|
||||||
|
file_path: "./src/package.json"
|
||||||
|
|
||||||
|
- name: Commit files
|
||||||
|
run: |
|
||||||
|
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||||
|
git config --local user.name "github-actions[bot]"
|
||||||
|
git commit -m "Bumped version to ${{ github.event.inputs.version_number }}" -a
|
||||||
|
|
||||||
|
- name: Push changes
|
||||||
|
run: git push -u origin version_bump_${{ github.event.inputs.version_number }}
|
||||||
|
|
||||||
|
- name: Create Version PR
|
||||||
|
env:
|
||||||
|
PR_BRANCH: "version_bump_${{ github.event.inputs.version_number }}"
|
||||||
|
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
|
BASE_BRANCH: master
|
||||||
|
TITLE: "Bump version to ${{ github.event.inputs.version_number }}"
|
||||||
|
run: |
|
||||||
|
gh pr create --title "$TITLE" \
|
||||||
|
--base "$BASE" \
|
||||||
|
--head "$PR_BRANCH" \
|
||||||
|
--label "version update" \
|
||||||
|
--label "automated pr" \
|
||||||
|
--body "
|
||||||
|
## Type of change
|
||||||
|
- [ ] Bug fix
|
||||||
|
- [ ] New feature development
|
||||||
|
- [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc)
|
||||||
|
- [ ] Build/deploy pipeline (DevOps)
|
||||||
|
- [X] Other
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
Automated version bump to ${{ github.event.inputs.version_number }}"
|
||||||
1
.husky/.gitignore
vendored
Normal file
1
.husky/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
_
|
||||||
4
.husky/pre-commit
Normal file
4
.husky/pre-commit
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
. "$(dirname "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
npx lint-staged
|
||||||
11
.prettierignore
Normal file
11
.prettierignore
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# Build directories
|
||||||
|
build
|
||||||
|
dist
|
||||||
|
|
||||||
|
jslib
|
||||||
|
|
||||||
|
# External libraries / auto synced locales
|
||||||
|
src/locales
|
||||||
|
|
||||||
|
# Github Workflows
|
||||||
|
.github/workflows
|
||||||
3
.prettierrc.json
Normal file
3
.prettierrc.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"printWidth": 100
|
||||||
|
}
|
||||||
66
.vscode/launch.json
vendored
66
.vscode/launch.json
vendored
@@ -1,48 +1,40 @@
|
|||||||
{
|
{
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
"type": "node",
|
"type": "node",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"name": "Electron: Main",
|
"name": "Electron: Main",
|
||||||
"protocol": "inspector",
|
"protocol": "inspector",
|
||||||
"cwd": "${workspaceRoot}/build",
|
"cwd": "${workspaceRoot}/build",
|
||||||
"runtimeArgs": [
|
"runtimeArgs": ["--remote-debugging-port=9223", "."],
|
||||||
"--remote-debugging-port=9223",
|
"windows": {
|
||||||
"."
|
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
|
||||||
],
|
|
||||||
"windows": {
|
|
||||||
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
|
|
||||||
},
|
|
||||||
"sourceMaps": true
|
|
||||||
},
|
},
|
||||||
{
|
"sourceMaps": true
|
||||||
"name": "Electron: Renderer",
|
|
||||||
"type": "chrome",
|
|
||||||
"request": "attach",
|
|
||||||
"port": 9223,
|
|
||||||
"webRoot": "${workspaceFolder}/build",
|
|
||||||
"sourceMaps": true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "node",
|
"name": "Electron: Renderer",
|
||||||
"request": "launch",
|
"type": "chrome",
|
||||||
"name": "Debug CLI",
|
"request": "attach",
|
||||||
"protocol": "inspector",
|
"port": 9223,
|
||||||
"cwd": "${workspaceFolder}",
|
"webRoot": "${workspaceFolder}/build",
|
||||||
"program": "${workspaceFolder}/build-cli/bwdc.js",
|
"sourceMaps": true
|
||||||
"args": [
|
},
|
||||||
"sync"
|
{
|
||||||
]
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Debug CLI",
|
||||||
|
"protocol": "inspector",
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"program": "${workspaceFolder}/build-cli/bwdc.js",
|
||||||
|
"args": ["sync"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"compounds": [
|
"compounds": [
|
||||||
{
|
{
|
||||||
"name": "Electron: All",
|
"name": "Electron: All",
|
||||||
"configurations": [
|
"configurations": ["Electron: Main", "Electron: Renderer"]
|
||||||
"Electron: Main",
|
}
|
||||||
"Electron: Renderer"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
23
README.md
23
README.md
@@ -6,6 +6,7 @@
|
|||||||
The Bitwarden Directory Connector is a a desktop application used to sync your Bitwarden enterprise organization to an existing directory of users and groups.
|
The Bitwarden Directory Connector is a a desktop application used to sync your Bitwarden enterprise organization to an existing directory of users and groups.
|
||||||
|
|
||||||
Supported directories:
|
Supported directories:
|
||||||
|
|
||||||
- Active Directory
|
- Active Directory
|
||||||
- Any other LDAP-based directory
|
- Any other LDAP-based directory
|
||||||
- Azure Active Directory
|
- Azure Active Directory
|
||||||
@@ -47,7 +48,7 @@ We provide detailed documentation and examples for using the Directory Connector
|
|||||||
|
|
||||||
**Requirements**
|
**Requirements**
|
||||||
|
|
||||||
- [Node.js](https://nodejs.org) v14
|
- [Node.js](https://nodejs.org) v16.13.1 (LTS)
|
||||||
- Windows users: To compile the native node modules used in the app you will need the Visual C++ toolset, available through the standard Visual Studio installer (recommended) or by installing [`windows-build-tools`](https://github.com/felixrieseberg/windows-build-tools) through `npm`. See more at [Compiling native Addon modules](https://github.com/Microsoft/nodejs-guidelines/blob/master/windows-environment.md#compiling-native-addon-modules).
|
- Windows users: To compile the native node modules used in the app you will need the Visual C++ toolset, available through the standard Visual Studio installer (recommended) or by installing [`windows-build-tools`](https://github.com/felixrieseberg/windows-build-tools) through `npm`. See more at [Compiling native Addon modules](https://github.com/Microsoft/nodejs-guidelines/blob/master/windows-environment.md#compiling-native-addon-modules).
|
||||||
|
|
||||||
**Run the app**
|
**Run the app**
|
||||||
@@ -78,3 +79,23 @@ node ./build-cli/bwdc.js --help
|
|||||||
Code contributions are welcome! Please commit any pull requests against the `master` branch. Learn more about how to contribute by reading the [`CONTRIBUTING.md`](CONTRIBUTING.md) file.
|
Code contributions are welcome! Please commit any pull requests against the `master` branch. Learn more about how to contribute by reading the [`CONTRIBUTING.md`](CONTRIBUTING.md) file.
|
||||||
|
|
||||||
Security audits and feedback are welcome. Please open an issue or email us privately if the report is sensitive in nature. You can read our security policy in the [`SECURITY.md`](SECURITY.md) file.
|
Security audits and feedback are welcome. Please open an issue or email us privately if the report is sensitive in nature. You can read our security policy in the [`SECURITY.md`](SECURITY.md) file.
|
||||||
|
|
||||||
|
### Prettier
|
||||||
|
|
||||||
|
We recently migrated to using Prettier as code formatter. All previous branches will need to updated to avoid large merge conflicts using the following steps:
|
||||||
|
|
||||||
|
1. Check out your local Branch
|
||||||
|
2. Run `git merge 225073aa335d33ad905877b68336a9288e89ea10`
|
||||||
|
3. Resolve any merge conflicts, commit.
|
||||||
|
4. Run `npm run prettier`
|
||||||
|
5. Commit
|
||||||
|
6. Run `git merge -Xours 096196fcd512944d1c3d9c007647a1319b032639`
|
||||||
|
7. Push
|
||||||
|
|
||||||
|
#### Git blame
|
||||||
|
|
||||||
|
We also recommend that you configure git to ignore the prettier revision using:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git config blame.ignoreRevsFile .git-blame-ignore-revs
|
||||||
|
```
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ notify us. We welcome working with you to resolve the issue promptly. Thanks in
|
|||||||
- Let us know as soon as possible upon discovery of a potential security issue, and we'll make every
|
- Let us know as soon as possible upon discovery of a potential security issue, and we'll make every
|
||||||
effort to quickly resolve the issue.
|
effort to quickly resolve the issue.
|
||||||
- Provide us a reasonable amount of time to resolve the issue before any disclosure to the public or a
|
- Provide us a reasonable amount of time to resolve the issue before any disclosure to the public or a
|
||||||
third-party. We may publicly disclose the issue before resolving it, if appropriate.
|
third-party. We may publicly disclose the issue before resolving it, if appropriate.
|
||||||
- Make a good faith effort to avoid privacy violations, destruction of data, and interruption or
|
- Make a good faith effort to avoid privacy violations, destruction of data, and interruption or
|
||||||
degradation of our service. Only interact with accounts you own or with explicit permission of the
|
degradation of our service. Only interact with accounts you own or with explicit permission of the
|
||||||
account holder.
|
account holder.
|
||||||
|
|||||||
2
jslib
2
jslib
Submodule jslib updated: d28e762c55...e0cc754d6f
16218
package-lock.json
generated
16218
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
84
package.json
84
package.json
@@ -1,6 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "bitwarden-directory-connector",
|
"name": "@bitwarden/directory-connector",
|
||||||
"productName": "Bitwarden Directory Connector",
|
|
||||||
"description": "Sync your user directory to your Bitwarden organization.",
|
"description": "Sync your user directory to your Bitwarden organization.",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
@@ -27,7 +26,7 @@
|
|||||||
"symlink:lin": "rm -rf ./jslib && ln -s ../jslib ./jslib",
|
"symlink:lin": "rm -rf ./jslib && ln -s ../jslib ./jslib",
|
||||||
"rebuild": "electron-rebuild",
|
"rebuild": "electron-rebuild",
|
||||||
"reset": "rimraf ./node_modules/keytar/* && npm install",
|
"reset": "rimraf ./node_modules/keytar/* && npm install",
|
||||||
"lint": "tslint 'src/**/*.ts' || true",
|
"lint": "tslint 'src/**/*.ts' && prettier --check .",
|
||||||
"lint:fix": "tslint 'src/**/*.ts' --fix",
|
"lint:fix": "tslint 'src/**/*.ts' --fix",
|
||||||
"build": "concurrently -n Main,Rend -c yellow,cyan \"npm run build:main\" \"npm run build:renderer\"",
|
"build": "concurrently -n Main,Rend -c yellow,cyan \"npm run build:main\" \"npm run build:renderer\"",
|
||||||
"build:main": "webpack --config webpack.main.js",
|
"build:main": "webpack --config webpack.main.js",
|
||||||
@@ -59,11 +58,17 @@
|
|||||||
"dist:cli:lin": "npm run build:cli:prod && npm run clean:dist:cli && npm run pack:cli:lin",
|
"dist:cli:lin": "npm run build:cli:prod && npm run clean:dist:cli && npm run pack:cli:lin",
|
||||||
"publish:lin": "npm run build:dist && npm run clean:dist && electron-builder --linux --x64 -p always",
|
"publish:lin": "npm run build:dist && npm run clean:dist && electron-builder --linux --x64 -p always",
|
||||||
"publish:mac": "npm run build:dist && npm run clean:dist && electron-builder --mac -p always",
|
"publish:mac": "npm run build:dist && npm run clean:dist && electron-builder --mac -p always",
|
||||||
"publish:win": "npm run build:dist && npm run clean:dist && electron-builder --win --x64 --ia32 -p always -c.win.certificateSubjectName=\"8bit Solutions LLC\""
|
"publish:win": "npm run build:dist && npm run clean:dist && electron-builder --win --x64 --ia32 -p always -c.win.certificateSubjectName=\"8bit Solutions LLC\"",
|
||||||
|
"prettier": "prettier --write .",
|
||||||
|
"prepare": "husky install"
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
|
"extraMetadata": {
|
||||||
|
"name": "bitwarden-directory-connector"
|
||||||
|
},
|
||||||
|
"productName": "Bitwarden Directory Connector",
|
||||||
"appId": "com.bitwarden.directory-connector",
|
"appId": "com.bitwarden.directory-connector",
|
||||||
"copyright": "Copyright © 2015-2020 Bitwarden Inc.",
|
"copyright": "Copyright © 2015-2022 Bitwarden Inc.",
|
||||||
"directories": {
|
"directories": {
|
||||||
"buildResources": "resources",
|
"buildResources": "resources",
|
||||||
"output": "dist",
|
"output": "dist",
|
||||||
@@ -132,51 +137,59 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular/compiler-cli": "^11.2.11",
|
"@angular/compiler-cli": "^12.2.13",
|
||||||
"@microsoft/microsoft-graph-types": "^1.4.0",
|
"@microsoft/microsoft-graph-types": "^1.4.0",
|
||||||
"@ngtools/webpack": "^11.2.10",
|
"@ngtools/webpack": "^12.2.13",
|
||||||
"@types/ldapjs": "^1.0.10",
|
"@types/ldapjs": "^1.0.10",
|
||||||
"@types/node": "^14.14.43",
|
"@types/node": "^16.11.12",
|
||||||
"@types/proper-lockfile": "^4.1.1",
|
"@types/proper-lockfile": "^4.1.1",
|
||||||
"clean-webpack-plugin": "^3.0.0",
|
"clean-webpack-plugin": "^4.0.0",
|
||||||
"concurrently": "^6.0.2",
|
"concurrently": "^6.0.2",
|
||||||
"copy-webpack-plugin": "^6.4.0",
|
"copy-webpack-plugin": "^10.0.0",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"css-loader": "^5.2.4",
|
"css-loader": "^6.5.1",
|
||||||
"del": "^6.0.0",
|
"electron-builder": "^22.14.5",
|
||||||
"electron-builder": "^22.11.7",
|
|
||||||
"electron-notarize": "^1.1.1",
|
"electron-notarize": "^1.1.1",
|
||||||
"electron-rebuild": "^3.2.3",
|
"electron-rebuild": "^3.2.5",
|
||||||
"electron-reload": "^1.5.0",
|
"electron-reload": "^1.5.0",
|
||||||
"file-loader": "^6.2.0",
|
"html-loader": "^3.0.1",
|
||||||
"font-awesome": "4.7.0",
|
"html-webpack-plugin": "^5.5.0",
|
||||||
"html-loader": "^1.3.2",
|
"husky": "^7.0.4",
|
||||||
"html-webpack-plugin": "^4.5.1",
|
"lint-staged": "^12.1.3",
|
||||||
"mini-css-extract-plugin": "^1.5.0",
|
"mini-css-extract-plugin": "^2.4.5",
|
||||||
"node-loader": "^1.0.3",
|
"node-loader": "^2.0.0",
|
||||||
"pkg": "^5.1.0",
|
"pkg": "^5.5.1",
|
||||||
|
"prebuild-install": "^5.0.0",
|
||||||
|
"prettier": "^2.5.1",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"sass": "^1.32.11",
|
"sass": "^1.32.11",
|
||||||
"sass-loader": "^10.1.1",
|
"sass-loader": "^12.4.0",
|
||||||
"tapable": "^1.1.3",
|
"tapable": "^1.1.3",
|
||||||
"ts-loader": "^8.1.0",
|
"ts-loader": "^9.2.5",
|
||||||
"tsconfig-paths-webpack-plugin": "^3.5.1",
|
"tsconfig-paths-webpack-plugin": "^3.5.1",
|
||||||
"tslint": "~6.1.0",
|
"tslint": "~6.1.0",
|
||||||
"tslint-loader": "^3.5.4",
|
"tslint-loader": "^3.5.4",
|
||||||
"typescript": "4.1.5",
|
"typescript": "4.3.5",
|
||||||
"webpack": "^4.46.0",
|
"webpack": "^5.64.4",
|
||||||
"webpack-cli": "^4.6.0",
|
"webpack-cli": "^4.9.1",
|
||||||
"webpack-merge": "^5.7.3",
|
"webpack-merge": "^5.8.0",
|
||||||
"webpack-node-externals": "^3.0.0",
|
"webpack-node-externals": "^3.0.0"
|
||||||
"prebuild-install": "^5.0.0"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@angular/animations": "^12.2.13",
|
||||||
|
"@angular/cdk": "^12.2.13",
|
||||||
|
"@angular/common": "^12.2.13",
|
||||||
|
"@angular/compiler": "^12.2.13",
|
||||||
|
"@angular/core": "^12.2.13",
|
||||||
|
"@angular/forms": "^12.2.13",
|
||||||
|
"@angular/platform-browser": "^12.2.13",
|
||||||
|
"@angular/platform-browser-dynamic": "^12.2.13",
|
||||||
|
"@angular/router": "^12.2.13",
|
||||||
"@bitwarden/jslib-angular": "file:jslib/angular",
|
"@bitwarden/jslib-angular": "file:jslib/angular",
|
||||||
"@bitwarden/jslib-common": "file:jslib/common",
|
"@bitwarden/jslib-common": "file:jslib/common",
|
||||||
"@bitwarden/jslib-electron": "file:jslib/electron",
|
"@bitwarden/jslib-electron": "file:jslib/electron",
|
||||||
"@bitwarden/jslib-node": "file:jslib/node",
|
"@bitwarden/jslib-node": "file:jslib/node",
|
||||||
"@microsoft/microsoft-graph-client": "^2.2.1",
|
"@microsoft/microsoft-graph-client": "^2.2.1",
|
||||||
"angular2-toaster": "^11.0.1",
|
|
||||||
"bootstrap": "^4.6.0",
|
"bootstrap": "^4.6.0",
|
||||||
"chalk": "^4.1.1",
|
"chalk": "^4.1.1",
|
||||||
"commander": "^7.2.0",
|
"commander": "^7.2.0",
|
||||||
@@ -187,11 +200,16 @@
|
|||||||
"inquirer": "8.0.0",
|
"inquirer": "8.0.0",
|
||||||
"ldapjs": "2.3.1",
|
"ldapjs": "2.3.1",
|
||||||
"lunr": "^2.3.9",
|
"lunr": "^2.3.9",
|
||||||
|
"ngx-toastr": "14.1.4",
|
||||||
"open": "^8.0.6",
|
"open": "^8.0.6",
|
||||||
"proper-lockfile": "^4.1.2"
|
"proper-lockfile": "^4.1.2",
|
||||||
|
"rxjs": "^7.4.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "~14",
|
"node": "~16",
|
||||||
"npm": "~7"
|
"npm": "~8"
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*": "prettier --ignore-unknown --write"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
require('dotenv').config();
|
require("dotenv").config();
|
||||||
const { notarize } = require('electron-notarize');
|
const { notarize } = require("electron-notarize");
|
||||||
|
|
||||||
exports.default = async function notarizing(context) {
|
exports.default = async function notarizing(context) {
|
||||||
const { electronPlatformName, appOutDir } = context;
|
const { electronPlatformName, appOutDir } = context;
|
||||||
if (electronPlatformName !== 'darwin') {
|
if (electronPlatformName !== "darwin") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const appleId = process.env.APPLE_ID_USERNAME || process.env.APPLEID;
|
const appleId = process.env.APPLE_ID_USERNAME || process.env.APPLEID;
|
||||||
const appleIdPassword = process.env.APPLE_ID_PASSWORD || `@keychain:AC_PASSWORD`;
|
const appleIdPassword = process.env.APPLE_ID_PASSWORD || `@keychain:AC_PASSWORD`;
|
||||||
const appName = context.packager.appInfo.productFilename;
|
const appName = context.packager.appInfo.productFilename;
|
||||||
return await notarize({
|
return await notarize({
|
||||||
appBundleId: 'com.bitwarden.directory-connector',
|
appBundleId: "com.bitwarden.directory-connector",
|
||||||
appPath: `${appOutDir}/${appName}.app`,
|
appPath: `${appOutDir}/${appName}.app`,
|
||||||
appleId: appleId,
|
appleId: appleId,
|
||||||
appleIdPassword: appleIdPassword,
|
appleIdPassword: appleIdPassword,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,22 +1,19 @@
|
|||||||
exports.default = async function(configuration) {
|
exports.default = async function (configuration) {
|
||||||
if (
|
if (parseInt(process.env.ELECTRON_BUILDER_SIGN) === 1 && configuration.path.slice(-4) == ".exe") {
|
||||||
parseInt(process.env.ELECTRON_BUILDER_SIGN) === 1 &&
|
console.log(`[*] Signing file: ${configuration.path}`);
|
||||||
configuration.path.slice(-4) == ".exe"
|
|
||||||
) {
|
|
||||||
console.log(`[*] Signing file: ${configuration.path}`)
|
|
||||||
require("child_process").execSync(
|
require("child_process").execSync(
|
||||||
`azuresigntool sign ` +
|
`azuresigntool sign ` +
|
||||||
`-kvu ${process.env.SIGNING_VAULT_URL} ` +
|
`-kvu ${process.env.SIGNING_VAULT_URL} ` +
|
||||||
`-kvi ${process.env.SIGNING_CLIENT_ID} ` +
|
`-kvi ${process.env.SIGNING_CLIENT_ID} ` +
|
||||||
`-kvt ${process.env.SIGNING_TENANT_ID} ` +
|
`-kvt ${process.env.SIGNING_TENANT_ID} ` +
|
||||||
`-kvs ${process.env.SIGNING_CLIENT_SECRET} ` +
|
`-kvs ${process.env.SIGNING_CLIENT_SECRET} ` +
|
||||||
`-kvc ${process.env.SIGNING_CERT_NAME} ` +
|
`-kvc ${process.env.SIGNING_CERT_NAME} ` +
|
||||||
`-fd ${configuration.hash} ` +
|
`-fd ${configuration.hash} ` +
|
||||||
`-du ${configuration.site} ` +
|
`-du ${configuration.site} ` +
|
||||||
`-tr http://timestamp.digicert.com ` +
|
`-tr http://timestamp.digicert.com ` +
|
||||||
`"${configuration.path}"`,
|
`"${configuration.path}"`,
|
||||||
{
|
{
|
||||||
stdio: "inherit"
|
stdio: "inherit",
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
1006
src-cli/package-lock.json
generated
Normal file
1006
src-cli/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "bitwarden-directory-connector",
|
"name": "@bitwarden/directory-connector",
|
||||||
"productName": "Bitwarden Directory Connector",
|
"productName": "Bitwarden Directory Connector",
|
||||||
"description": "Sync your user directory to your Bitwarden organization.",
|
"description": "Sync your user directory to your Bitwarden organization.",
|
||||||
"version": "2.9.5",
|
"version": "2.9.5",
|
||||||
@@ -19,6 +19,6 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"browser-hrtime": "^1.1.8",
|
"browser-hrtime": "^1.1.8",
|
||||||
"keytar": "7.6.0"
|
"keytar": "^7.7.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
68
src/abstractions/state.service.ts
Normal file
68
src/abstractions/state.service.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { StateService as BaseStateServiceAbstraction } from "jslib-common/abstractions/state.service";
|
||||||
|
|
||||||
|
import { StorageOptions } from "jslib-common/models/domain/storageOptions";
|
||||||
|
|
||||||
|
import { DirectoryType } from "src/enums/directoryType";
|
||||||
|
|
||||||
|
import { Account } from "src/models/account";
|
||||||
|
import { AzureConfiguration } from "src/models/azureConfiguration";
|
||||||
|
import { GSuiteConfiguration } from "src/models/gsuiteConfiguration";
|
||||||
|
import { LdapConfiguration } from "src/models/ldapConfiguration";
|
||||||
|
import { OktaConfiguration } from "src/models/oktaConfiguration";
|
||||||
|
import { OneLoginConfiguration } from "src/models/oneLoginConfiguration";
|
||||||
|
import { SyncConfiguration } from "src/models/syncConfiguration";
|
||||||
|
|
||||||
|
export abstract class StateService extends BaseStateServiceAbstraction<Account> {
|
||||||
|
getDirectory: <IConfiguration>(type: DirectoryType) => Promise<IConfiguration>;
|
||||||
|
setDirectory: (
|
||||||
|
type: DirectoryType,
|
||||||
|
config:
|
||||||
|
| LdapConfiguration
|
||||||
|
| GSuiteConfiguration
|
||||||
|
| AzureConfiguration
|
||||||
|
| OktaConfiguration
|
||||||
|
| OneLoginConfiguration
|
||||||
|
) => Promise<any>;
|
||||||
|
getLdapKey: (options?: StorageOptions) => Promise<string>;
|
||||||
|
setLdapKey: (value: string, options?: StorageOptions) => Promise<void>;
|
||||||
|
getGsuiteKey: (options?: StorageOptions) => Promise<string>;
|
||||||
|
setGsuiteKey: (value: string, options?: StorageOptions) => Promise<void>;
|
||||||
|
getAzureKey: (options?: StorageOptions) => Promise<string>;
|
||||||
|
setAzureKey: (value: string, options?: StorageOptions) => Promise<void>;
|
||||||
|
getOktaKey: (options?: StorageOptions) => Promise<string>;
|
||||||
|
setOktaKey: (value: string, options?: StorageOptions) => Promise<void>;
|
||||||
|
getOneLoginKey: (options?: StorageOptions) => Promise<string>;
|
||||||
|
setOneLoginKey: (value: string, options?: StorageOptions) => Promise<void>;
|
||||||
|
getLdapConfiguration: (options?: StorageOptions) => Promise<LdapConfiguration>;
|
||||||
|
setLdapConfiguration: (value: LdapConfiguration, options?: StorageOptions) => Promise<void>;
|
||||||
|
getGsuiteConfiguration: (options?: StorageOptions) => Promise<GSuiteConfiguration>;
|
||||||
|
setGsuiteConfiguration: (value: GSuiteConfiguration, options?: StorageOptions) => Promise<void>;
|
||||||
|
getAzureConfiguration: (options?: StorageOptions) => Promise<AzureConfiguration>;
|
||||||
|
setAzureConfiguration: (value: AzureConfiguration, options?: StorageOptions) => Promise<void>;
|
||||||
|
getOktaConfiguration: (options?: StorageOptions) => Promise<OktaConfiguration>;
|
||||||
|
setOktaConfiguration: (value: OktaConfiguration, options?: StorageOptions) => Promise<void>;
|
||||||
|
getOneLoginConfiguration: (options?: StorageOptions) => Promise<OneLoginConfiguration>;
|
||||||
|
setOneLoginConfiguration: (
|
||||||
|
value: OneLoginConfiguration,
|
||||||
|
options?: StorageOptions
|
||||||
|
) => Promise<void>;
|
||||||
|
getOrganizationId: (options?: StorageOptions) => Promise<string>;
|
||||||
|
setOrganizationId: (value: string, options?: StorageOptions) => Promise<void>;
|
||||||
|
getSync: (options?: StorageOptions) => Promise<SyncConfiguration>;
|
||||||
|
setSync: (value: SyncConfiguration, options?: StorageOptions) => Promise<void>;
|
||||||
|
getDirectoryType: (options?: StorageOptions) => Promise<DirectoryType>;
|
||||||
|
setDirectoryType: (value: DirectoryType, options?: StorageOptions) => Promise<void>;
|
||||||
|
getUserDelta: (options?: StorageOptions) => Promise<string>;
|
||||||
|
setUserDelta: (value: string, options?: StorageOptions) => Promise<void>;
|
||||||
|
getLastUserSync: (options?: StorageOptions) => Promise<Date>;
|
||||||
|
setLastUserSync: (value: Date, options?: StorageOptions) => Promise<void>;
|
||||||
|
getLastGroupSync: (options?: StorageOptions) => Promise<Date>;
|
||||||
|
setLastGroupSync: (value: Date, options?: StorageOptions) => Promise<void>;
|
||||||
|
getGroupDelta: (options?: StorageOptions) => Promise<string>;
|
||||||
|
setGroupDelta: (value: string, options?: StorageOptions) => Promise<void>;
|
||||||
|
getLastSyncHash: (options?: StorageOptions) => Promise<string>;
|
||||||
|
setLastSyncHash: (value: string, options?: StorageOptions) => Promise<void>;
|
||||||
|
getSyncingDir: (options?: StorageOptions) => Promise<boolean>;
|
||||||
|
setSyncingDir: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||||
|
clearSyncSettings: (syncHashToo: boolean) => Promise<void>;
|
||||||
|
}
|
||||||
@@ -1,47 +1,60 @@
|
|||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise">
|
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise">
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<div class="col-md-8 col-lg-6">
|
<div class="col-md-8 col-lg-6">
|
||||||
<p class="text-center font-weight-bold">{{'welcome' | i18n}}</p>
|
<p class="text-center font-weight-bold">{{ "welcome" | i18n }}</p>
|
||||||
<p class="text-center">{{'logInDesc' | i18n}}</p>
|
<p class="text-center">{{ "logInDesc" | i18n }}</p>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">{{'logIn' | i18n}}</h5>
|
<h5 class="card-header">{{ "logIn" | i18n }}</h5>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="client_id">{{'clientId' | i18n}}</label>
|
<label for="client_id">{{ "clientId" | i18n }}</label>
|
||||||
<input id="client_id" name="ClientId" [(ngModel)]="clientId"
|
<input id="client_id" name="ClientId" [(ngModel)]="clientId" class="form-control" />
|
||||||
class="form-control">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="row-main">
|
|
||||||
<label for="client_secret">{{'clientSecret' | i18n}}</label>
|
|
||||||
<div class="input-group">
|
|
||||||
<input type="{{showSecret ? 'text' : 'password'}}" id="client_secret" name="ClientSecret"
|
|
||||||
[(ngModel)]="clientSecret" class="form-control">
|
|
||||||
<div class="input-group-append">
|
|
||||||
<button type="button" class="ml-1 btn btn-link" appA11yTitle="{{'toggleVisibility' | i18n}}" (click)="toggleSecret()">
|
|
||||||
<i class="fa fa-lg" aria-hidden="true"[ngClass]="showSecret ? 'fa-eye-slash' : 'fa-eye'"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex">
|
|
||||||
<div>
|
|
||||||
<button type="submit" class="btn btn-primary" [disabled]="form.loading">
|
|
||||||
<i class="fa fa-spinner fa-fw fa-spin" [hidden]="!form.loading"></i>
|
|
||||||
<i class="fa fa-sign-in fa-fw" [hidden]="form.loading"></i>
|
|
||||||
{{'logIn' | i18n}}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<button type="button" class="btn btn-link ml-auto" (click)="settings()">
|
|
||||||
{{'settings' | i18n}}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="row-main">
|
||||||
|
<label for="client_secret">{{ "clientSecret" | i18n }}</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input
|
||||||
|
type="{{ showSecret ? 'text' : 'password' }}"
|
||||||
|
id="client_secret"
|
||||||
|
name="ClientSecret"
|
||||||
|
[(ngModel)]="clientSecret"
|
||||||
|
class="form-control"
|
||||||
|
/>
|
||||||
|
<div class="input-group-append">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="ml-1 btn btn-link"
|
||||||
|
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
||||||
|
(click)="toggleSecret()"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="bwi bwi-lg"
|
||||||
|
aria-hidden="true"
|
||||||
|
[ngClass]="showSecret ? 'bwi-eye-slash' : 'bwi-eye'"
|
||||||
|
></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex">
|
||||||
|
<div>
|
||||||
|
<button type="submit" class="btn btn-primary" [disabled]="form.loading">
|
||||||
|
<i class="bwi bwi-spinner bwi-fw bwi-spin" [hidden]="!form.loading"></i>
|
||||||
|
<i class="bwi bwi-sign-in bwi-fw" [hidden]="form.loading"></i>
|
||||||
|
{{ "logIn" | i18n }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-link ml-auto" (click)="settings()">
|
||||||
|
{{ "settings" | i18n }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<ng-template #environment></ng-template>
|
<ng-template #environment></ng-template>
|
||||||
|
|||||||
@@ -1,84 +1,103 @@
|
|||||||
import {
|
import { Component, Input, ViewChild, ViewContainerRef } from "@angular/core";
|
||||||
Component,
|
import { Router } from "@angular/router";
|
||||||
ComponentFactoryResolver,
|
|
||||||
Input,
|
|
||||||
ViewChild,
|
|
||||||
ViewContainerRef,
|
|
||||||
} from '@angular/core';
|
|
||||||
import { Router } from '@angular/router';
|
|
||||||
|
|
||||||
import { EnvironmentComponent } from './environment.component';
|
import { EnvironmentComponent } from "./environment.component";
|
||||||
|
|
||||||
import { ApiKeyService } from 'jslib-common/abstractions/apiKey.service';
|
import { AuthService } from "jslib-common/abstractions/auth.service";
|
||||||
import { AuthService } from 'jslib-common/abstractions/auth.service';
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
import { LogService } from "jslib-common/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
|
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
|
|
||||||
import { ModalService } from 'jslib-angular/services/modal.service';
|
import { StateService } from "../../abstractions/state.service";
|
||||||
|
|
||||||
import { Utils } from 'jslib-common/misc/utils';
|
import { ModalService } from "jslib-angular/services/modal.service";
|
||||||
import { ConfigurationService } from '../../services/configuration.service';
|
|
||||||
|
import { HtmlStorageLocation } from "jslib-common/enums/htmlStorageLocation";
|
||||||
|
import { Utils } from "jslib-common/misc/utils";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-apiKey',
|
selector: "app-apiKey",
|
||||||
templateUrl: 'apiKey.component.html',
|
templateUrl: "apiKey.component.html",
|
||||||
})
|
})
|
||||||
export class ApiKeyComponent {
|
export class ApiKeyComponent {
|
||||||
@ViewChild('environment', { read: ViewContainerRef, static: true }) environmentModal: ViewContainerRef;
|
@ViewChild("environment", { read: ViewContainerRef, static: true })
|
||||||
@Input() clientId: string = '';
|
environmentModal: ViewContainerRef;
|
||||||
@Input() clientSecret: string = '';
|
@Input() clientId: string = "";
|
||||||
|
@Input() clientSecret: string = "";
|
||||||
|
|
||||||
formPromise: Promise<any>;
|
formPromise: Promise<any>;
|
||||||
successRoute = '/tabs/dashboard';
|
successRoute = "/tabs/dashboard";
|
||||||
showSecret: boolean = false;
|
showSecret: boolean = false;
|
||||||
|
|
||||||
constructor(private authService: AuthService, private apiKeyService: ApiKeyService, private router: Router,
|
constructor(
|
||||||
private i18nService: I18nService, private componentFactoryResolver: ComponentFactoryResolver,
|
private authService: AuthService,
|
||||||
private configurationService: ConfigurationService, private platformUtilsService: PlatformUtilsService,
|
private router: Router,
|
||||||
private modalService: ModalService) { }
|
private i18nService: I18nService,
|
||||||
|
private platformUtilsService: PlatformUtilsService,
|
||||||
|
private modalService: ModalService,
|
||||||
|
private logService: LogService,
|
||||||
|
private stateService: StateService
|
||||||
|
) {}
|
||||||
|
|
||||||
async submit() {
|
async submit() {
|
||||||
if (this.clientId == null || this.clientId === '') {
|
if (this.clientId == null || this.clientId === "") {
|
||||||
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
|
this.platformUtilsService.showToast(
|
||||||
this.i18nService.t('clientIdRequired'));
|
"error",
|
||||||
return;
|
this.i18nService.t("errorOccurred"),
|
||||||
}
|
this.i18nService.t("clientIdRequired")
|
||||||
if (!this.clientId.startsWith('organization')) {
|
);
|
||||||
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
|
return;
|
||||||
this.i18nService.t('orgApiKeyRequired'));
|
}
|
||||||
return;
|
if (!this.clientId.startsWith("organization")) {
|
||||||
}
|
this.platformUtilsService.showToast(
|
||||||
if (this.clientSecret == null || this.clientSecret === '') {
|
"error",
|
||||||
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
|
this.i18nService.t("errorOccurred"),
|
||||||
this.i18nService.t('clientSecretRequired'));
|
this.i18nService.t("orgApiKeyRequired")
|
||||||
return;
|
);
|
||||||
}
|
return;
|
||||||
const idParts = this.clientId.split('.');
|
}
|
||||||
|
if (this.clientSecret == null || this.clientSecret === "") {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
this.i18nService.t("errorOccurred"),
|
||||||
|
this.i18nService.t("clientSecretRequired")
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const idParts = this.clientId.split(".");
|
||||||
|
|
||||||
if (idParts.length !== 2 || idParts[0] !== 'organization' || !Utils.isGuid(idParts[1])) {
|
if (idParts.length !== 2 || idParts[0] !== "organization" || !Utils.isGuid(idParts[1])) {
|
||||||
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
|
this.platformUtilsService.showToast(
|
||||||
this.i18nService.t('invalidClientId'));
|
"error",
|
||||||
return;
|
this.i18nService.t("errorOccurred"),
|
||||||
}
|
this.i18nService.t("invalidClientId")
|
||||||
|
);
|
||||||
try {
|
return;
|
||||||
this.formPromise = this.authService.logInApiKey(this.clientId, this.clientSecret);
|
|
||||||
await this.formPromise;
|
|
||||||
const organizationId = await this.apiKeyService.getEntityId();
|
|
||||||
await this.configurationService.saveOrganizationId(organizationId);
|
|
||||||
this.router.navigate([this.successRoute]);
|
|
||||||
} catch { }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async settings() {
|
try {
|
||||||
const [modalRef, childComponent] = await this.modalService.openViewRef(EnvironmentComponent, this.environmentModal);
|
this.formPromise = this.authService.logInApiKey(this.clientId, this.clientSecret);
|
||||||
|
await this.formPromise;
|
||||||
|
const organizationId = await this.stateService.getEntityId();
|
||||||
|
await this.stateService.setOrganizationId(organizationId);
|
||||||
|
this.router.navigate([this.successRoute]);
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
childComponent.onSaved.subscribe(() => {
|
async settings() {
|
||||||
modalRef.close();
|
const [modalRef, childComponent] = await this.modalService.openViewRef(
|
||||||
});
|
EnvironmentComponent,
|
||||||
}
|
this.environmentModal
|
||||||
toggleSecret() {
|
);
|
||||||
this.showSecret = !this.showSecret;
|
|
||||||
document.getElementById('client_secret').focus();
|
childComponent.onSaved.subscribe(() => {
|
||||||
}
|
modalRef.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
toggleSecret() {
|
||||||
|
this.showSecret = !this.showSecret;
|
||||||
|
document.getElementById("client_secret").focus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,43 +1,61 @@
|
|||||||
<div class="modal fade">
|
<div class="modal fade">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<form class="modal-content" (ngSubmit)="submit()">
|
<form class="modal-content" (ngSubmit)="submit()">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h3 class="modal-title">{{'settings' | i18n}}</h3>
|
<h3 class="modal-title">{{ "settings" | i18n }}</h3>
|
||||||
<button type="button" class="close" data-dismiss="modal" title="Close">
|
<button type="button" class="close" data-dismiss="modal" title="Close">
|
||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<h4>{{'selfHostedEnvironment' | i18n}}</h4>
|
<h4>{{ "selfHostedEnvironment" | i18n }}</h4>
|
||||||
<p>{{'selfHostedEnvironmentFooter' | i18n}}</p>
|
<p>{{ "selfHostedEnvironmentFooter" | i18n }}</p>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="baseUrl">{{'baseUrl' | i18n}}</label>
|
<label for="baseUrl">{{ "baseUrl" | i18n }}</label>
|
||||||
<input id="baseUrl" type="text" name="BaseUrl" [(ngModel)]="baseUrl" class="form-control">
|
<input
|
||||||
<small class="text-muted form-text">{{'ex' | i18n}} https://bitwarden.company.com</small>
|
id="baseUrl"
|
||||||
</div>
|
type="text"
|
||||||
<h4>{{'customEnvironment' | i18n}}</h4>
|
name="BaseUrl"
|
||||||
<p>{{'customEnvironmentFooter' | i18n}}</p>
|
[(ngModel)]="baseUrl"
|
||||||
<div class="form-group">
|
class="form-control"
|
||||||
<label for="webVaultUrl">{{'webVaultUrl' | i18n}}</label>
|
/>
|
||||||
<input id="webVaultUrl" type="text" name="WebVaultUrl" [(ngModel)]="webVaultUrl"
|
<small class="text-muted form-text"
|
||||||
class="form-control">
|
>{{ "ex" | i18n }} https://bitwarden.company.com</small
|
||||||
</div>
|
>
|
||||||
<div class="form-group">
|
</div>
|
||||||
<label for="apiUrl">{{'apiUrl' | i18n}}</label>
|
<h4>{{ "customEnvironment" | i18n }}</h4>
|
||||||
<input id="apiUrl" type="text" name="ApiUrl" [(ngModel)]="apiUrl" class="form-control">
|
<p>{{ "customEnvironmentFooter" | i18n }}</p>
|
||||||
</div>
|
<div class="form-group">
|
||||||
<div class="form-group">
|
<label for="webVaultUrl">{{ "webVaultUrl" | i18n }}</label>
|
||||||
<label for="identityUrl">{{'identityUrl' | i18n}}</label>
|
<input
|
||||||
<input id="identityUrl" type="text" name="IdentityUrl" [(ngModel)]="identityUrl"
|
id="webVaultUrl"
|
||||||
class="form-control">
|
type="text"
|
||||||
</div>
|
name="WebVaultUrl"
|
||||||
</div>
|
[(ngModel)]="webVaultUrl"
|
||||||
<div class="modal-footer justify-content-start">
|
class="form-control"
|
||||||
<button type="submit" class="btn btn-primary">
|
/>
|
||||||
<i class="fa fa-save fa-fw"></i>
|
</div>
|
||||||
{{'save' | i18n}}
|
<div class="form-group">
|
||||||
</button>
|
<label for="apiUrl">{{ "apiUrl" | i18n }}</label>
|
||||||
</div>
|
<input id="apiUrl" type="text" name="ApiUrl" [(ngModel)]="apiUrl" class="form-control" />
|
||||||
</form>
|
</div>
|
||||||
</div>
|
<div class="form-group">
|
||||||
|
<label for="identityUrl">{{ "identityUrl" | i18n }}</label>
|
||||||
|
<input
|
||||||
|
id="identityUrl"
|
||||||
|
type="text"
|
||||||
|
name="IdentityUrl"
|
||||||
|
[(ngModel)]="identityUrl"
|
||||||
|
class="form-control"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer justify-content-start">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="bwi bwi-save bwi-fw"></i>
|
||||||
|
{{ "save" | i18n }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,18 +1,21 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from "@angular/core";
|
||||||
|
|
||||||
import { EnvironmentService } from 'jslib-common/abstractions/environment.service';
|
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
|
||||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
|
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
|
|
||||||
import { EnvironmentComponent as BaseEnvironmentComponent } from 'jslib-angular/components/environment.component';
|
import { EnvironmentComponent as BaseEnvironmentComponent } from "jslib-angular/components/environment.component";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-environment',
|
selector: "app-environment",
|
||||||
templateUrl: 'environment.component.html',
|
templateUrl: "environment.component.html",
|
||||||
})
|
})
|
||||||
export class EnvironmentComponent extends BaseEnvironmentComponent {
|
export class EnvironmentComponent extends BaseEnvironmentComponent {
|
||||||
constructor(environmentService: EnvironmentService, i18nService: I18nService,
|
constructor(
|
||||||
platformUtilsService: PlatformUtilsService) {
|
environmentService: EnvironmentService,
|
||||||
super(platformUtilsService, environmentService, i18nService);
|
i18nService: I18nService,
|
||||||
}
|
platformUtilsService: PlatformUtilsService
|
||||||
|
) {
|
||||||
|
super(platformUtilsService, environmentService, i18nService);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,58 +1,57 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from "@angular/core";
|
||||||
import {
|
import { RouterModule, Routes } from "@angular/router";
|
||||||
RouterModule,
|
|
||||||
Routes,
|
|
||||||
} from '@angular/router';
|
|
||||||
|
|
||||||
import { AuthGuardService } from './services/auth-guard.service';
|
import { AuthGuardService } from "./services/auth-guard.service";
|
||||||
import { LaunchGuardService } from './services/launch-guard.service';
|
import { LaunchGuardService } from "./services/launch-guard.service";
|
||||||
|
|
||||||
import { ApiKeyComponent } from './accounts/apiKey.component';
|
import { ApiKeyComponent } from "./accounts/apiKey.component";
|
||||||
import { DashboardComponent } from './tabs/dashboard.component';
|
import { DashboardComponent } from "./tabs/dashboard.component";
|
||||||
import { MoreComponent } from './tabs/more.component';
|
import { MoreComponent } from "./tabs/more.component";
|
||||||
import { SettingsComponent } from './tabs/settings.component';
|
import { SettingsComponent } from "./tabs/settings.component";
|
||||||
import { TabsComponent } from './tabs/tabs.component';
|
import { TabsComponent } from "./tabs/tabs.component";
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{ path: '', redirectTo: '/login', pathMatch: 'full' },
|
{ path: "", redirectTo: "/login", pathMatch: "full" },
|
||||||
{
|
{
|
||||||
path: 'login',
|
path: "login",
|
||||||
component: ApiKeyComponent,
|
component: ApiKeyComponent,
|
||||||
canActivate: [LaunchGuardService],
|
canActivate: [LaunchGuardService],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'tabs',
|
path: "tabs",
|
||||||
component: TabsComponent,
|
component: TabsComponent,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: "",
|
||||||
redirectTo: '/tabs/dashboard',
|
redirectTo: "/tabs/dashboard",
|
||||||
pathMatch: 'full',
|
pathMatch: "full",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'dashboard',
|
path: "dashboard",
|
||||||
component: DashboardComponent,
|
component: DashboardComponent,
|
||||||
canActivate: [AuthGuardService],
|
canActivate: [AuthGuardService],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'settings',
|
path: "settings",
|
||||||
component: SettingsComponent,
|
component: SettingsComponent,
|
||||||
canActivate: [AuthGuardService],
|
canActivate: [AuthGuardService],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'more',
|
path: "more",
|
||||||
component: MoreComponent,
|
component: MoreComponent,
|
||||||
canActivate: [AuthGuardService],
|
canActivate: [AuthGuardService],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [RouterModule.forRoot(routes, {
|
imports: [
|
||||||
useHash: true,
|
RouterModule.forRoot(routes, {
|
||||||
/*enableTracing: true,*/
|
useHash: true,
|
||||||
})],
|
/*enableTracing: true,*/
|
||||||
exports: [RouterModule],
|
}),
|
||||||
|
],
|
||||||
|
exports: [RouterModule],
|
||||||
})
|
})
|
||||||
export class AppRoutingModule { }
|
export class AppRoutingModule {}
|
||||||
|
|||||||
@@ -1,172 +1,161 @@
|
|||||||
import {
|
import {
|
||||||
BodyOutputType,
|
Component,
|
||||||
Toast,
|
NgZone,
|
||||||
ToasterConfig,
|
OnInit,
|
||||||
ToasterContainerComponent,
|
SecurityContext,
|
||||||
ToasterService,
|
ViewChild,
|
||||||
} from 'angular2-toaster';
|
ViewContainerRef,
|
||||||
|
} from "@angular/core";
|
||||||
|
import { DomSanitizer } from "@angular/platform-browser";
|
||||||
|
import { Router } from "@angular/router";
|
||||||
|
import { IndividualConfig, ToastrService } from "ngx-toastr";
|
||||||
|
|
||||||
import {
|
import { AuthService } from "jslib-common/abstractions/auth.service";
|
||||||
Component,
|
import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service";
|
||||||
ComponentFactoryResolver,
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
NgZone,
|
import { LogService } from "jslib-common/abstractions/log.service";
|
||||||
OnInit,
|
import { MessagingService } from "jslib-common/abstractions/messaging.service";
|
||||||
SecurityContext,
|
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
Type,
|
import { TokenService } from "jslib-common/abstractions/token.service";
|
||||||
ViewChild,
|
|
||||||
ViewContainerRef,
|
|
||||||
} from '@angular/core';
|
|
||||||
import { DomSanitizer } from '@angular/platform-browser';
|
|
||||||
import { Router } from '@angular/router';
|
|
||||||
|
|
||||||
import { BroadcasterService } from 'jslib-angular/services/broadcaster.service';
|
import { SyncService } from "../services/sync.service";
|
||||||
|
|
||||||
import { ApiService } from 'jslib-common/abstractions/api.service';
|
import { StateService } from "../abstractions/state.service";
|
||||||
import { AuthService } from 'jslib-common/abstractions/auth.service';
|
|
||||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
|
||||||
import { MessagingService } from 'jslib-common/abstractions/messaging.service';
|
|
||||||
import { StateService } from 'jslib-common/abstractions/state.service';
|
|
||||||
import { StorageService } from 'jslib-common/abstractions/storage.service';
|
|
||||||
import { TokenService } from 'jslib-common/abstractions/token.service';
|
|
||||||
import { UserService } from 'jslib-common/abstractions/user.service';
|
|
||||||
|
|
||||||
import { ConfigurationService } from '../services/configuration.service';
|
const BroadcasterSubscriptionId = "AppComponent";
|
||||||
import { SyncService } from '../services/sync.service';
|
|
||||||
|
|
||||||
const BroadcasterSubscriptionId = 'AppComponent';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: "app-root",
|
||||||
styles: [],
|
styles: [],
|
||||||
template: `
|
template: ` <ng-template #settings></ng-template>
|
||||||
<toaster-container [toasterconfig]="toasterConfig"></toaster-container>
|
<router-outlet></router-outlet>`,
|
||||||
<ng-template #settings></ng-template>
|
|
||||||
<router-outlet></router-outlet>`,
|
|
||||||
})
|
})
|
||||||
export class AppComponent implements OnInit {
|
export class AppComponent implements OnInit {
|
||||||
@ViewChild('settings', { read: ViewContainerRef, static: true }) settingsRef: ViewContainerRef;
|
@ViewChild("settings", { read: ViewContainerRef, static: true }) settingsRef: ViewContainerRef;
|
||||||
|
|
||||||
toasterConfig: ToasterConfig = new ToasterConfig({
|
constructor(
|
||||||
showCloseButton: true,
|
private broadcasterService: BroadcasterService,
|
||||||
mouseoverTimerStop: true,
|
private tokenService: TokenService,
|
||||||
animation: 'flyRight',
|
private authService: AuthService,
|
||||||
limit: 5,
|
private router: Router,
|
||||||
});
|
private toastrService: ToastrService,
|
||||||
|
private i18nService: I18nService,
|
||||||
|
private sanitizer: DomSanitizer,
|
||||||
|
private ngZone: NgZone,
|
||||||
|
private platformUtilsService: PlatformUtilsService,
|
||||||
|
private messagingService: MessagingService,
|
||||||
|
private syncService: SyncService,
|
||||||
|
private stateService: StateService,
|
||||||
|
private logService: LogService
|
||||||
|
) {}
|
||||||
|
|
||||||
private lastActivity: number = null;
|
ngOnInit() {
|
||||||
|
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
|
||||||
|
this.ngZone.run(async () => {
|
||||||
|
switch (message.command) {
|
||||||
|
case "syncScheduleStarted":
|
||||||
|
case "syncScheduleStopped":
|
||||||
|
this.stateService.setSyncingDir(message.command === "syncScheduleStarted");
|
||||||
|
break;
|
||||||
|
case "logout":
|
||||||
|
this.logOut(!!message.expired);
|
||||||
|
break;
|
||||||
|
case "checkDirSync":
|
||||||
|
try {
|
||||||
|
const syncConfig = await this.stateService.getSync();
|
||||||
|
if (syncConfig.interval == null || syncConfig.interval < 5) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(private broadcasterService: BroadcasterService, private userService: UserService,
|
const syncInterval = syncConfig.interval * 60000;
|
||||||
private tokenService: TokenService, private storageService: StorageService,
|
const lastGroupSync = await this.stateService.getLastGroupSync();
|
||||||
private authService: AuthService, private router: Router,
|
const lastUserSync = await this.stateService.getLastUserSync();
|
||||||
private toasterService: ToasterService, private i18nService: I18nService,
|
let lastSync: Date = null;
|
||||||
private sanitizer: DomSanitizer, private ngZone: NgZone,
|
if (lastGroupSync != null && lastUserSync == null) {
|
||||||
private componentFactoryResolver: ComponentFactoryResolver, private messagingService: MessagingService,
|
lastSync = lastGroupSync;
|
||||||
private configurationService: ConfigurationService, private syncService: SyncService,
|
} else if (lastGroupSync == null && lastUserSync != null) {
|
||||||
private stateService: StateService, private apiService: ApiService) {
|
lastSync = lastUserSync;
|
||||||
(window as any).BitwardenToasterService = toasterService;
|
} else if (lastGroupSync != null && lastUserSync != null) {
|
||||||
}
|
if (lastGroupSync.getTime() < lastUserSync.getTime()) {
|
||||||
|
lastSync = lastGroupSync;
|
||||||
ngOnInit() {
|
} else {
|
||||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
|
lastSync = lastUserSync;
|
||||||
this.ngZone.run(async () => {
|
|
||||||
switch (message.command) {
|
|
||||||
case 'syncScheduleStarted':
|
|
||||||
case 'syncScheduleStopped':
|
|
||||||
this.stateService.save('syncingDir', message.command === 'syncScheduleStarted');
|
|
||||||
break;
|
|
||||||
case 'logout':
|
|
||||||
this.logOut(!!message.expired);
|
|
||||||
break;
|
|
||||||
case 'checkDirSync':
|
|
||||||
try {
|
|
||||||
const syncConfig = await this.configurationService.getSync();
|
|
||||||
if (syncConfig.interval == null || syncConfig.interval < 5) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const syncInterval = syncConfig.interval * 60000;
|
|
||||||
const lastGroupSync = await this.configurationService.getLastGroupSyncDate();
|
|
||||||
const lastUserSync = await this.configurationService.getLastUserSyncDate();
|
|
||||||
let lastSync: Date = null;
|
|
||||||
if (lastGroupSync != null && lastUserSync == null) {
|
|
||||||
lastSync = lastGroupSync;
|
|
||||||
} else if (lastGroupSync == null && lastUserSync != null) {
|
|
||||||
lastSync = lastUserSync;
|
|
||||||
} else if (lastGroupSync != null && lastUserSync != null) {
|
|
||||||
if (lastGroupSync.getTime() < lastUserSync.getTime()) {
|
|
||||||
lastSync = lastGroupSync;
|
|
||||||
} else {
|
|
||||||
lastSync = lastUserSync;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let lastSyncAgo = syncInterval + 1;
|
|
||||||
if (lastSync != null) {
|
|
||||||
lastSyncAgo = new Date().getTime() - lastSync.getTime();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lastSyncAgo >= syncInterval) {
|
|
||||||
await this.syncService.sync(false, false);
|
|
||||||
}
|
|
||||||
} catch { }
|
|
||||||
|
|
||||||
this.messagingService.send('scheduleNextDirSync');
|
|
||||||
break;
|
|
||||||
case 'showToast':
|
|
||||||
this.showToast(message);
|
|
||||||
break;
|
|
||||||
case 'ssoCallback':
|
|
||||||
this.router.navigate(['sso'], { queryParams: { code: message.code, state: message.state } });
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let lastSyncAgo = syncInterval + 1;
|
||||||
|
if (lastSync != null) {
|
||||||
|
lastSyncAgo = new Date().getTime() - lastSync.getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastSyncAgo >= syncInterval) {
|
||||||
|
await this.syncService.sync(false, false);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.messagingService.send("scheduleNextDirSync");
|
||||||
|
break;
|
||||||
|
case "showToast":
|
||||||
|
this.showToast(message);
|
||||||
|
break;
|
||||||
|
case "ssoCallback":
|
||||||
|
this.router.navigate(["sso"], {
|
||||||
|
queryParams: { code: message.code, state: message.state },
|
||||||
});
|
});
|
||||||
});
|
break;
|
||||||
}
|
default:
|
||||||
|
|
||||||
ngOnDestroy() {
|
|
||||||
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async logOut(expired: boolean) {
|
|
||||||
const userId = await this.userService.getUserId();
|
|
||||||
|
|
||||||
await this.tokenService.clearToken();
|
|
||||||
await this.userService.clear();
|
|
||||||
|
|
||||||
this.authService.logOut(async () => {
|
|
||||||
if (expired) {
|
|
||||||
this.toasterService.popAsync('warning', this.i18nService.t('loggedOut'),
|
|
||||||
this.i18nService.t('loginExpired'));
|
|
||||||
}
|
|
||||||
this.router.navigate(['login']);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private showToast(msg: any) {
|
|
||||||
const toast: Toast = {
|
|
||||||
type: msg.type,
|
|
||||||
title: msg.title,
|
|
||||||
};
|
|
||||||
if (typeof (msg.text) === 'string') {
|
|
||||||
toast.body = msg.text;
|
|
||||||
} else if (msg.text.length === 1) {
|
|
||||||
toast.body = msg.text[0];
|
|
||||||
} else {
|
|
||||||
let message = '';
|
|
||||||
msg.text.forEach((t: string) =>
|
|
||||||
message += ('<p>' + this.sanitizer.sanitize(SecurityContext.HTML, t) + '</p>'));
|
|
||||||
toast.body = message;
|
|
||||||
toast.bodyOutputType = BodyOutputType.TrustedHtml;
|
|
||||||
}
|
}
|
||||||
if (msg.options != null) {
|
});
|
||||||
if (msg.options.trustedHtml === true) {
|
});
|
||||||
toast.bodyOutputType = BodyOutputType.TrustedHtml;
|
}
|
||||||
}
|
|
||||||
if (msg.options.timeout != null && msg.options.timeout > 0) {
|
ngOnDestroy() {
|
||||||
toast.timeout = msg.options.timeout;
|
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
this.toasterService.popAsync(toast);
|
private async logOut(expired: boolean) {
|
||||||
|
await this.tokenService.clearToken();
|
||||||
|
await this.stateService.clean();
|
||||||
|
|
||||||
|
this.authService.logOut(async () => {
|
||||||
|
if (expired) {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"warning",
|
||||||
|
this.i18nService.t("loggedOut"),
|
||||||
|
this.i18nService.t("loginExpired")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.router.navigate(["login"]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private showToast(msg: any) {
|
||||||
|
let message = "";
|
||||||
|
|
||||||
|
const options: Partial<IndividualConfig> = {};
|
||||||
|
|
||||||
|
if (typeof msg.text === "string") {
|
||||||
|
message = msg.text;
|
||||||
|
} else if (msg.text.length === 1) {
|
||||||
|
message = msg.text[0];
|
||||||
|
} else {
|
||||||
|
msg.text.forEach(
|
||||||
|
(t: string) =>
|
||||||
|
(message += "<p>" + this.sanitizer.sanitize(SecurityContext.HTML, t) + "</p>")
|
||||||
|
);
|
||||||
|
options.enableHtml = true;
|
||||||
}
|
}
|
||||||
|
if (msg.options != null) {
|
||||||
|
if (msg.options.trustedHtml === true) {
|
||||||
|
options.enableHtml = true;
|
||||||
|
}
|
||||||
|
if (msg.options.timeout != null && msg.options.timeout > 0) {
|
||||||
|
options.timeOut = msg.options.timeout;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.toastrService.show(message, msg.title, options, "toast-" + msg.type);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,71 +1,74 @@
|
|||||||
import 'core-js/stable';
|
import "core-js/stable";
|
||||||
import 'zone.js/dist/zone';
|
import "zone.js/dist/zone";
|
||||||
|
|
||||||
import { ToasterModule } from 'angular2-toaster';
|
import { AppRoutingModule } from "./app-routing.module";
|
||||||
|
import { ServicesModule } from "./services/services.module";
|
||||||
|
|
||||||
import { AppRoutingModule } from './app-routing.module';
|
import { NgModule } from "@angular/core";
|
||||||
import { ServicesModule } from './services/services.module';
|
import { FormsModule } from "@angular/forms";
|
||||||
|
import { BrowserModule } from "@angular/platform-browser";
|
||||||
|
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
|
||||||
|
|
||||||
import { NgModule } from '@angular/core';
|
import { AppComponent } from "./app.component";
|
||||||
import { FormsModule } from '@angular/forms';
|
|
||||||
import { BrowserModule } from '@angular/platform-browser';
|
|
||||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
|
||||||
|
|
||||||
import { AppComponent } from './app.component';
|
import { CalloutComponent } from "jslib-angular/components/callout.component";
|
||||||
|
import { IconComponent } from "jslib-angular/components/icon.component";
|
||||||
|
import { BitwardenToastModule } from "jslib-angular/components/toastr.component";
|
||||||
|
|
||||||
import { CalloutComponent } from 'jslib-angular/components/callout.component';
|
import { ApiKeyComponent } from "./accounts/apiKey.component";
|
||||||
import { IconComponent } from 'jslib-angular/components/icon.component';
|
import { EnvironmentComponent } from "./accounts/environment.component";
|
||||||
|
import { DashboardComponent } from "./tabs/dashboard.component";
|
||||||
|
import { MoreComponent } from "./tabs/more.component";
|
||||||
|
import { SettingsComponent } from "./tabs/settings.component";
|
||||||
|
import { TabsComponent } from "./tabs/tabs.component";
|
||||||
|
|
||||||
import { ApiKeyComponent } from './accounts/apiKey.component';
|
import { A11yTitleDirective } from "jslib-angular/directives/a11y-title.directive";
|
||||||
import { EnvironmentComponent } from './accounts/environment.component';
|
import { ApiActionDirective } from "jslib-angular/directives/api-action.directive";
|
||||||
import { DashboardComponent } from './tabs/dashboard.component';
|
import { AutofocusDirective } from "jslib-angular/directives/autofocus.directive";
|
||||||
import { MoreComponent } from './tabs/more.component';
|
import { BlurClickDirective } from "jslib-angular/directives/blur-click.directive";
|
||||||
import { SettingsComponent } from './tabs/settings.component';
|
import { BoxRowDirective } from "jslib-angular/directives/box-row.directive";
|
||||||
import { TabsComponent } from './tabs/tabs.component';
|
import { FallbackSrcDirective } from "jslib-angular/directives/fallback-src.directive";
|
||||||
|
import { StopClickDirective } from "jslib-angular/directives/stop-click.directive";
|
||||||
|
import { StopPropDirective } from "jslib-angular/directives/stop-prop.directive";
|
||||||
|
|
||||||
import { A11yTitleDirective } from 'jslib-angular/directives/a11y-title.directive';
|
import { I18nPipe } from "jslib-angular/pipes/i18n.pipe";
|
||||||
import { ApiActionDirective } from 'jslib-angular/directives/api-action.directive';
|
import { SearchCiphersPipe } from "jslib-angular/pipes/search-ciphers.pipe";
|
||||||
import { AutofocusDirective } from 'jslib-angular/directives/autofocus.directive';
|
|
||||||
import { BlurClickDirective } from 'jslib-angular/directives/blur-click.directive';
|
|
||||||
import { BoxRowDirective } from 'jslib-angular/directives/box-row.directive';
|
|
||||||
import { FallbackSrcDirective } from 'jslib-angular/directives/fallback-src.directive';
|
|
||||||
import { StopClickDirective } from 'jslib-angular/directives/stop-click.directive';
|
|
||||||
import { StopPropDirective } from 'jslib-angular/directives/stop-prop.directive';
|
|
||||||
|
|
||||||
import { I18nPipe } from 'jslib-angular/pipes/i18n.pipe';
|
|
||||||
import { SearchCiphersPipe } from 'jslib-angular/pipes/search-ciphers.pipe';
|
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
BrowserAnimationsModule,
|
BrowserAnimationsModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
AppRoutingModule,
|
AppRoutingModule,
|
||||||
ServicesModule,
|
ServicesModule,
|
||||||
ToasterModule.forRoot(),
|
BitwardenToastModule.forRoot({
|
||||||
],
|
maxOpened: 5,
|
||||||
declarations: [
|
autoDismiss: true,
|
||||||
A11yTitleDirective,
|
closeButton: true,
|
||||||
ApiActionDirective,
|
}),
|
||||||
ApiKeyComponent,
|
],
|
||||||
AppComponent,
|
declarations: [
|
||||||
AutofocusDirective,
|
A11yTitleDirective,
|
||||||
BlurClickDirective,
|
ApiActionDirective,
|
||||||
BoxRowDirective,
|
ApiKeyComponent,
|
||||||
CalloutComponent,
|
AppComponent,
|
||||||
DashboardComponent,
|
AutofocusDirective,
|
||||||
EnvironmentComponent,
|
BlurClickDirective,
|
||||||
FallbackSrcDirective,
|
BoxRowDirective,
|
||||||
I18nPipe,
|
CalloutComponent,
|
||||||
IconComponent,
|
DashboardComponent,
|
||||||
MoreComponent,
|
EnvironmentComponent,
|
||||||
SearchCiphersPipe,
|
FallbackSrcDirective,
|
||||||
SettingsComponent,
|
I18nPipe,
|
||||||
StopClickDirective,
|
IconComponent,
|
||||||
StopPropDirective,
|
MoreComponent,
|
||||||
TabsComponent,
|
SearchCiphersPipe,
|
||||||
],
|
SettingsComponent,
|
||||||
providers: [],
|
StopClickDirective,
|
||||||
bootstrap: [AppComponent],
|
StopPropDirective,
|
||||||
|
TabsComponent,
|
||||||
|
],
|
||||||
|
providers: [],
|
||||||
|
bootstrap: [AppComponent],
|
||||||
})
|
})
|
||||||
export class AppModule { }
|
export class AppModule {}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { enableProdMode } from '@angular/core';
|
import { enableProdMode } from "@angular/core";
|
||||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
|
||||||
|
|
||||||
import { isDev } from 'jslib-electron/utils';
|
import { isDev } from "jslib-electron/utils";
|
||||||
|
|
||||||
// tslint:disable-next-line
|
// tslint:disable-next-line
|
||||||
require('../scss/styles.scss');
|
require("../scss/styles.scss");
|
||||||
|
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from "./app.module";
|
||||||
|
|
||||||
if (!isDev()) {
|
if (!isDev()) {
|
||||||
enableProdMode();
|
enableProdMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
platformBrowserDynamic().bootstrapModule(AppModule, { preserveWhitespaces: true });
|
platformBrowserDynamic().bootstrapModule(AppModule, { preserveWhitespaces: true });
|
||||||
|
|||||||
@@ -1,24 +1,21 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from "@angular/core";
|
||||||
import {
|
import { CanActivate } from "@angular/router";
|
||||||
CanActivate,
|
|
||||||
Router,
|
|
||||||
} from '@angular/router';
|
|
||||||
import { ApiKeyService } from 'jslib-common/abstractions/apiKey.service';
|
|
||||||
|
|
||||||
import { MessagingService } from 'jslib-common/abstractions/messaging.service';
|
import { MessagingService } from "jslib-common/abstractions/messaging.service";
|
||||||
|
|
||||||
|
import { StateService } from "../../abstractions/state.service";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthGuardService implements CanActivate {
|
export class AuthGuardService implements CanActivate {
|
||||||
constructor(private apiKeyService: ApiKeyService, private router: Router,
|
constructor(private stateService: StateService, private messagingService: MessagingService) {}
|
||||||
private messagingService: MessagingService) { }
|
|
||||||
|
|
||||||
async canActivate() {
|
async canActivate() {
|
||||||
const isAuthed = await this.apiKeyService.isAuthenticated();
|
const isAuthed = await this.stateService.getIsAuthenticated();
|
||||||
if (!isAuthed) {
|
if (!isAuthed) {
|
||||||
this.messagingService.send('logout');
|
this.messagingService.send("logout");
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,19 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from "@angular/core";
|
||||||
import {
|
import { CanActivate, Router } from "@angular/router";
|
||||||
CanActivate,
|
|
||||||
Router,
|
|
||||||
} from '@angular/router';
|
|
||||||
|
|
||||||
import { ApiKeyService } from 'jslib-common/abstractions/apiKey.service';
|
import { StateService } from "../../abstractions/state.service";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LaunchGuardService implements CanActivate {
|
export class LaunchGuardService implements CanActivate {
|
||||||
constructor(private apiKeyService: ApiKeyService, private router: Router) { }
|
constructor(private stateService: StateService, private router: Router) {}
|
||||||
|
|
||||||
async canActivate() {
|
async canActivate() {
|
||||||
const isAuthed = await this.apiKeyService.isAuthenticated();
|
const isAuthed = await this.stateService.getIsAuthenticated();
|
||||||
if (!isAuthed) {
|
if (!isAuthed) {
|
||||||
return true;
|
return true;
|
||||||
}
|
|
||||||
|
|
||||||
this.router.navigate(['/tabs/dashboard']);
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.router.navigate(["/tabs/dashboard"]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,165 +1,237 @@
|
|||||||
import {
|
import { APP_INITIALIZER, Injector, NgModule } from "@angular/core";
|
||||||
APP_INITIALIZER,
|
|
||||||
NgModule,
|
|
||||||
} from '@angular/core';
|
|
||||||
|
|
||||||
import { ToasterModule } from 'angular2-toaster';
|
import { ElectronLogService } from "jslib-electron/services/electronLog.service";
|
||||||
|
import { ElectronPlatformUtilsService } from "jslib-electron/services/electronPlatformUtils.service";
|
||||||
|
import { ElectronRendererMessagingService } from "jslib-electron/services/electronRendererMessaging.service";
|
||||||
|
import { ElectronRendererSecureStorageService } from "jslib-electron/services/electronRendererSecureStorage.service";
|
||||||
|
import { ElectronRendererStorageService } from "jslib-electron/services/electronRendererStorage.service";
|
||||||
|
|
||||||
import { ElectronLogService } from 'jslib-electron/services/electronLog.service';
|
import { AuthGuardService } from "./auth-guard.service";
|
||||||
import { ElectronPlatformUtilsService } from 'jslib-electron/services/electronPlatformUtils.service';
|
import { LaunchGuardService } from "./launch-guard.service";
|
||||||
import { ElectronRendererMessagingService } from 'jslib-electron/services/electronRendererMessaging.service';
|
|
||||||
import { ElectronRendererSecureStorageService } from 'jslib-electron/services/electronRendererSecureStorage.service';
|
|
||||||
import { ElectronRendererStorageService } from 'jslib-electron/services/electronRendererStorage.service';
|
|
||||||
|
|
||||||
import { AuthGuardService } from './auth-guard.service';
|
import { I18nService } from "../../services/i18n.service";
|
||||||
import { LaunchGuardService } from './launch-guard.service';
|
import { SyncService } from "../../services/sync.service";
|
||||||
|
|
||||||
import { ConfigurationService } from '../../services/configuration.service';
|
import { JslibServicesModule } from "jslib-angular/services/jslib-services.module";
|
||||||
import { I18nService } from '../../services/i18n.service';
|
|
||||||
import { SyncService } from '../../services/sync.service';
|
|
||||||
|
|
||||||
import { BroadcasterService } from 'jslib-angular/services/broadcaster.service';
|
import { ContainerService } from "jslib-common/services/container.service";
|
||||||
import { ModalService } from 'jslib-angular/services/modal.service';
|
|
||||||
import { ValidationService } from 'jslib-angular/services/validation.service';
|
|
||||||
|
|
||||||
import { ApiKeyService } from 'jslib-common/services/apiKey.service';
|
import { NodeApiService } from "jslib-node/services/nodeApi.service";
|
||||||
import { AppIdService } from 'jslib-common/services/appId.service';
|
import { NodeCryptoFunctionService } from "jslib-node/services/nodeCryptoFunction.service";
|
||||||
import { ConstantsService } from 'jslib-common/services/constants.service';
|
|
||||||
import { ContainerService } from 'jslib-common/services/container.service';
|
|
||||||
import { CryptoService } from 'jslib-common/services/crypto.service';
|
|
||||||
import { EnvironmentService } from 'jslib-common/services/environment.service';
|
|
||||||
import { PasswordGenerationService } from 'jslib-common/services/passwordGeneration.service';
|
|
||||||
import { PolicyService } from 'jslib-common/services/policy.service';
|
|
||||||
import { StateService } from 'jslib-common/services/state.service';
|
|
||||||
import { TokenService } from 'jslib-common/services/token.service';
|
|
||||||
import { UserService } from 'jslib-common/services/user.service';
|
|
||||||
|
|
||||||
import { NodeCryptoFunctionService } from 'jslib-node/services/nodeCryptoFunction.service';
|
import { ApiService as ApiServiceAbstraction } from "jslib-common/abstractions/api.service";
|
||||||
|
import { AppIdService as AppIdServiceAbstraction } from "jslib-common/abstractions/appId.service";
|
||||||
|
import { AuthService as AuthServiceAbstraction } from "jslib-common/abstractions/auth.service";
|
||||||
|
import { BroadcasterService as BroadcasterServiceAbstraction } from "jslib-common/abstractions/broadcaster.service";
|
||||||
|
import { CryptoService as CryptoServiceAbstraction } from "jslib-common/abstractions/crypto.service";
|
||||||
|
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "jslib-common/abstractions/cryptoFunction.service";
|
||||||
|
import { EnvironmentService as EnvironmentServiceAbstraction } from "jslib-common/abstractions/environment.service";
|
||||||
|
import { I18nService as I18nServiceAbstraction } from "jslib-common/abstractions/i18n.service";
|
||||||
|
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "jslib-common/abstractions/keyConnector.service";
|
||||||
|
import { LogService as LogServiceAbstraction } from "jslib-common/abstractions/log.service";
|
||||||
|
import { MessagingService as MessagingServiceAbstraction } from "jslib-common/abstractions/messaging.service";
|
||||||
|
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "jslib-common/abstractions/platformUtils.service";
|
||||||
|
import { StateMigrationService as StateMigrationServiceAbstraction } from "jslib-common/abstractions/stateMigration.service";
|
||||||
|
import { StorageService as StorageServiceAbstraction } from "jslib-common/abstractions/storage.service";
|
||||||
|
import { TokenService as TokenServiceAbstraction } from "jslib-common/abstractions/token.service";
|
||||||
|
import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "jslib-common/abstractions/vaultTimeout.service";
|
||||||
|
|
||||||
import { ApiService as ApiServiceAbstraction } from 'jslib-common/abstractions/api.service';
|
import { StateService as StateServiceAbstraction } from "../../abstractions/state.service";
|
||||||
import { ApiKeyService as ApiKeyServiceAbstraction } from 'jslib-common/abstractions/apiKey.service';
|
|
||||||
import { AuthService as AuthServiceAbstraction } from 'jslib-common/abstractions/auth.service';
|
|
||||||
import { CryptoService as CryptoServiceAbstraction } from 'jslib-common/abstractions/crypto.service';
|
|
||||||
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from 'jslib-common/abstractions/cryptoFunction.service';
|
|
||||||
import { EnvironmentService as EnvironmentServiceAbstraction } from 'jslib-common/abstractions/environment.service';
|
|
||||||
import { I18nService as I18nServiceAbstraction } from 'jslib-common/abstractions/i18n.service';
|
|
||||||
import { LogService as LogServiceAbstraction } from 'jslib-common/abstractions/log.service';
|
|
||||||
import { MessagingService as MessagingServiceAbstraction } from 'jslib-common/abstractions/messaging.service';
|
|
||||||
import {
|
|
||||||
PasswordGenerationService as PasswordGenerationServiceAbstraction,
|
|
||||||
} from 'jslib-common/abstractions/passwordGeneration.service';
|
|
||||||
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from 'jslib-common/abstractions/platformUtils.service';
|
|
||||||
import { PolicyService as PolicyServiceAbstraction } from 'jslib-common/abstractions/policy.service';
|
|
||||||
import { StateService as StateServiceAbstraction } from 'jslib-common/abstractions/state.service';
|
|
||||||
import { StorageService as StorageServiceAbstraction } from 'jslib-common/abstractions/storage.service';
|
|
||||||
import { TokenService as TokenServiceAbstraction } from 'jslib-common/abstractions/token.service';
|
|
||||||
import { UserService as UserServiceAbstraction } from 'jslib-common/abstractions/user.service';
|
|
||||||
|
|
||||||
import { ApiService, refreshToken } from '../../services/api.service';
|
import { ApiService, refreshToken } from "../../services/api.service";
|
||||||
import { AuthService } from '../../services/auth.service';
|
import { AuthService } from "../../services/auth.service";
|
||||||
|
import { StateService } from "../../services/state.service";
|
||||||
|
import { StateMigrationService } from "../../services/stateMigration.service";
|
||||||
|
|
||||||
const logService = new ElectronLogService();
|
import { Account } from "../../models/account";
|
||||||
const i18nService = new I18nService(window.navigator.language, './locales');
|
|
||||||
const stateService = new StateService();
|
|
||||||
const broadcasterService = new BroadcasterService();
|
|
||||||
const messagingService = new ElectronRendererMessagingService(broadcasterService);
|
|
||||||
const storageService: StorageServiceAbstraction = new ElectronRendererStorageService();
|
|
||||||
const platformUtilsService = new ElectronPlatformUtilsService(i18nService, messagingService, false, storageService);
|
|
||||||
const secureStorageService: StorageServiceAbstraction = new ElectronRendererSecureStorageService();
|
|
||||||
const cryptoFunctionService: CryptoFunctionServiceAbstraction = new NodeCryptoFunctionService();
|
|
||||||
const cryptoService = new CryptoService(storageService, secureStorageService, cryptoFunctionService,
|
|
||||||
platformUtilsService, logService);
|
|
||||||
const appIdService = new AppIdService(storageService);
|
|
||||||
const tokenService = new TokenService(storageService);
|
|
||||||
const environmentService = new EnvironmentService(storageService);
|
|
||||||
const apiService = new ApiService(tokenService, platformUtilsService, environmentService, refreshTokenCallback,
|
|
||||||
async (expired: boolean) => messagingService.send('logout', { expired: expired }));
|
|
||||||
const userService = new UserService(tokenService, storageService);
|
|
||||||
const apiKeyService = new ApiKeyService(tokenService, storageService);
|
|
||||||
const containerService = new ContainerService(cryptoService);
|
|
||||||
const authService = new AuthService(cryptoService, apiService, userService, tokenService, appIdService,
|
|
||||||
i18nService, platformUtilsService, messagingService, null, logService, apiKeyService, false);
|
|
||||||
const configurationService = new ConfigurationService(storageService, secureStorageService);
|
|
||||||
const syncService = new SyncService(configurationService, logService, cryptoFunctionService, apiService,
|
|
||||||
messagingService, i18nService, environmentService);
|
|
||||||
const passwordGenerationService = new PasswordGenerationService(cryptoService, storageService, null);
|
|
||||||
const policyService = new PolicyService(userService, storageService);
|
|
||||||
|
|
||||||
containerService.attachToWindow(window);
|
import { StateFactory } from "jslib-common/factories/stateFactory";
|
||||||
|
|
||||||
function refreshTokenCallback(): Promise<any> {
|
import { GlobalState } from "jslib-common/models/domain/globalState";
|
||||||
return refreshToken(apiKeyService, authService);
|
|
||||||
|
function refreshTokenCallback(injector: Injector) {
|
||||||
|
return () => {
|
||||||
|
const stateService = injector.get(StateServiceAbstraction);
|
||||||
|
const authService = injector.get(AuthServiceAbstraction);
|
||||||
|
return refreshToken(stateService, authService);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initFactory(): Function {
|
export function initFactory(
|
||||||
return async () => {
|
environmentService: EnvironmentServiceAbstraction,
|
||||||
await environmentService.setUrlsFromStorage();
|
i18nService: I18nService,
|
||||||
await i18nService.init();
|
authService: AuthService,
|
||||||
authService.init();
|
platformUtilsService: PlatformUtilsServiceAbstraction,
|
||||||
apiKeyService.migrateApiKeyStorage();
|
stateService: StateServiceAbstraction,
|
||||||
const htmlEl = window.document.documentElement;
|
cryptoService: CryptoServiceAbstraction
|
||||||
htmlEl.classList.add('os_' + platformUtilsService.getDeviceString());
|
): Function {
|
||||||
htmlEl.classList.add('locale_' + i18nService.translationLocale);
|
return async () => {
|
||||||
window.document.title = i18nService.t('bitwardenDirectoryConnector');
|
await stateService.init();
|
||||||
|
await environmentService.setUrlsFromStorage();
|
||||||
|
await i18nService.init();
|
||||||
|
authService.init();
|
||||||
|
const htmlEl = window.document.documentElement;
|
||||||
|
htmlEl.classList.add("os_" + platformUtilsService.getDeviceString());
|
||||||
|
htmlEl.classList.add("locale_" + i18nService.translationLocale);
|
||||||
|
window.document.title = i18nService.t("bitwardenDirectoryConnector");
|
||||||
|
|
||||||
let installAction = null;
|
let installAction = null;
|
||||||
const installedVersion = await storageService.get<string>(ConstantsService.installedVersionKey);
|
const installedVersion = await stateService.getInstalledVersion();
|
||||||
const currentVersion = await platformUtilsService.getApplicationVersion();
|
const currentVersion = await platformUtilsService.getApplicationVersion();
|
||||||
if (installedVersion == null) {
|
if (installedVersion == null) {
|
||||||
installAction = 'install';
|
installAction = "install";
|
||||||
} else if (installedVersion !== currentVersion) {
|
} else if (installedVersion !== currentVersion) {
|
||||||
installAction = 'update';
|
installAction = "update";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (installAction != null) {
|
if (installAction != null) {
|
||||||
await storageService.save(ConstantsService.installedVersionKey, currentVersion);
|
await stateService.setInstalledVersion(currentVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
window.setTimeout(async () => {
|
const containerService = new ContainerService(cryptoService);
|
||||||
if (await userService.isAuthenticated()) {
|
containerService.attachToWindow(window);
|
||||||
const profile = await apiService.getProfile();
|
};
|
||||||
stateService.save('profileOrganizations', profile.organizations);
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [JslibServicesModule],
|
||||||
ToasterModule,
|
declarations: [],
|
||||||
],
|
providers: [
|
||||||
declarations: [],
|
{
|
||||||
providers: [
|
provide: APP_INITIALIZER,
|
||||||
ValidationService,
|
useFactory: initFactory,
|
||||||
AuthGuardService,
|
deps: [
|
||||||
LaunchGuardService,
|
EnvironmentServiceAbstraction,
|
||||||
ModalService,
|
I18nServiceAbstraction,
|
||||||
{ provide: AuthServiceAbstraction, useValue: authService },
|
AuthServiceAbstraction,
|
||||||
{ provide: EnvironmentServiceAbstraction, useValue: environmentService },
|
PlatformUtilsServiceAbstraction,
|
||||||
{ provide: TokenServiceAbstraction, useValue: tokenService },
|
StateServiceAbstraction,
|
||||||
{ provide: I18nServiceAbstraction, useValue: i18nService },
|
CryptoServiceAbstraction,
|
||||||
{ provide: CryptoServiceAbstraction, useValue: cryptoService },
|
],
|
||||||
{ provide: PlatformUtilsServiceAbstraction, useValue: platformUtilsService },
|
multi: true,
|
||||||
{ provide: ApiServiceAbstraction, useValue: apiService },
|
},
|
||||||
{ provide: UserServiceAbstraction, useValue: userService },
|
{ provide: LogServiceAbstraction, useClass: ElectronLogService, deps: [] },
|
||||||
{ provide: ApiKeyServiceAbstraction, useValue: apiKeyService },
|
{
|
||||||
{ provide: MessagingServiceAbstraction, useValue: messagingService },
|
provide: I18nServiceAbstraction,
|
||||||
{ provide: BroadcasterService, useValue: broadcasterService },
|
useFactory: (window: Window) => new I18nService(window.navigator.language, "./locales"),
|
||||||
{ provide: StorageServiceAbstraction, useValue: storageService },
|
deps: ["WINDOW"],
|
||||||
{ provide: StateServiceAbstraction, useValue: stateService },
|
},
|
||||||
{ provide: LogServiceAbstraction, useValue: logService },
|
{
|
||||||
{ provide: ConfigurationService, useValue: configurationService },
|
provide: MessagingServiceAbstraction,
|
||||||
{ provide: SyncService, useValue: syncService },
|
useClass: ElectronRendererMessagingService,
|
||||||
{ provide: PasswordGenerationServiceAbstraction, useValue: passwordGenerationService },
|
deps: [BroadcasterServiceAbstraction],
|
||||||
{ provide: CryptoFunctionServiceAbstraction, useValue: cryptoFunctionService },
|
},
|
||||||
{ provide: PolicyServiceAbstraction, useValue: policyService },
|
{ provide: StorageServiceAbstraction, useClass: ElectronRendererStorageService },
|
||||||
{
|
{ provide: "SECURE_STORAGE", useClass: ElectronRendererSecureStorageService },
|
||||||
provide: APP_INITIALIZER,
|
{
|
||||||
useFactory: initFactory,
|
provide: PlatformUtilsServiceAbstraction,
|
||||||
deps: [],
|
useFactory: (
|
||||||
multi: true,
|
i18nService: I18nServiceAbstraction,
|
||||||
},
|
messagingService: MessagingServiceAbstraction,
|
||||||
],
|
stateService: StateServiceAbstraction
|
||||||
|
) => new ElectronPlatformUtilsService(i18nService, messagingService, true, stateService),
|
||||||
|
deps: [I18nServiceAbstraction, MessagingServiceAbstraction, StateServiceAbstraction],
|
||||||
|
},
|
||||||
|
{ provide: CryptoFunctionServiceAbstraction, useClass: NodeCryptoFunctionService, deps: [] },
|
||||||
|
{
|
||||||
|
provide: ApiServiceAbstraction,
|
||||||
|
useFactory: (
|
||||||
|
tokenService: TokenServiceAbstraction,
|
||||||
|
platformUtilsService: PlatformUtilsServiceAbstraction,
|
||||||
|
environmentService: EnvironmentServiceAbstraction,
|
||||||
|
messagingService: MessagingServiceAbstraction,
|
||||||
|
injector: Injector
|
||||||
|
) =>
|
||||||
|
new NodeApiService(
|
||||||
|
tokenService,
|
||||||
|
platformUtilsService,
|
||||||
|
environmentService,
|
||||||
|
async (expired: boolean) => messagingService.send("logout", { expired: expired }),
|
||||||
|
"Bitwarden_DC/" +
|
||||||
|
platformUtilsService.getApplicationVersion() +
|
||||||
|
" (" +
|
||||||
|
platformUtilsService.getDeviceString().toUpperCase() +
|
||||||
|
")",
|
||||||
|
refreshTokenCallback(injector)
|
||||||
|
),
|
||||||
|
deps: [
|
||||||
|
TokenServiceAbstraction,
|
||||||
|
PlatformUtilsServiceAbstraction,
|
||||||
|
EnvironmentServiceAbstraction,
|
||||||
|
MessagingServiceAbstraction,
|
||||||
|
Injector,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: AuthServiceAbstraction,
|
||||||
|
useClass: AuthService,
|
||||||
|
deps: [
|
||||||
|
CryptoServiceAbstraction,
|
||||||
|
ApiServiceAbstraction,
|
||||||
|
TokenServiceAbstraction,
|
||||||
|
AppIdServiceAbstraction,
|
||||||
|
I18nServiceAbstraction,
|
||||||
|
PlatformUtilsServiceAbstraction,
|
||||||
|
MessagingServiceAbstraction,
|
||||||
|
VaultTimeoutServiceAbstraction,
|
||||||
|
LogServiceAbstraction,
|
||||||
|
CryptoFunctionServiceAbstraction,
|
||||||
|
EnvironmentServiceAbstraction,
|
||||||
|
KeyConnectorServiceAbstraction,
|
||||||
|
StateServiceAbstraction,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: SyncService,
|
||||||
|
useClass: SyncService,
|
||||||
|
deps: [
|
||||||
|
LogServiceAbstraction,
|
||||||
|
CryptoFunctionServiceAbstraction,
|
||||||
|
ApiServiceAbstraction,
|
||||||
|
MessagingServiceAbstraction,
|
||||||
|
I18nServiceAbstraction,
|
||||||
|
EnvironmentServiceAbstraction,
|
||||||
|
StateServiceAbstraction,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
AuthGuardService,
|
||||||
|
LaunchGuardService,
|
||||||
|
{
|
||||||
|
provide: StateMigrationServiceAbstraction,
|
||||||
|
useFactory: (
|
||||||
|
storageService: StorageServiceAbstraction,
|
||||||
|
secureStorageService: StorageServiceAbstraction
|
||||||
|
) =>
|
||||||
|
new StateMigrationService(
|
||||||
|
storageService,
|
||||||
|
secureStorageService,
|
||||||
|
new StateFactory(GlobalState, Account)
|
||||||
|
),
|
||||||
|
deps: [StorageServiceAbstraction, "SECURE_STORAGE"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: StateServiceAbstraction,
|
||||||
|
useFactory: (
|
||||||
|
storageService: StorageServiceAbstraction,
|
||||||
|
secureStorageService: StorageServiceAbstraction,
|
||||||
|
logService: LogServiceAbstraction,
|
||||||
|
stateMigrationService: StateMigrationServiceAbstraction
|
||||||
|
) =>
|
||||||
|
new StateService(
|
||||||
|
storageService,
|
||||||
|
secureStorageService,
|
||||||
|
logService,
|
||||||
|
stateMigrationService,
|
||||||
|
true,
|
||||||
|
new StateFactory(GlobalState, Account)
|
||||||
|
),
|
||||||
|
deps: [
|
||||||
|
StorageServiceAbstraction,
|
||||||
|
"SECURE_STORAGE",
|
||||||
|
LogServiceAbstraction,
|
||||||
|
StateMigrationServiceAbstraction,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class ServicesModule {
|
export class ServicesModule {}
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,99 +1,110 @@
|
|||||||
<div class="card mb-3">
|
<div class="card mb-3">
|
||||||
<h3 class="card-header">{{'sync' | i18n}}</h3>
|
<h3 class="card-header">{{ "sync" | i18n }}</h3>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p>
|
<p>
|
||||||
{{'lastGroupSync' | i18n}}:
|
{{ "lastGroupSync" | i18n }}:
|
||||||
<span *ngIf="!lastGroupSync">-</span>
|
<span *ngIf="!lastGroupSync">-</span>
|
||||||
{{lastGroupSync | date:'medium'}}
|
{{ lastGroupSync | date: "medium" }}
|
||||||
<br /> {{'lastUserSync' | i18n}}:
|
<br />
|
||||||
<span *ngIf="!lastUserSync">-</span>
|
{{ "lastUserSync" | i18n }}:
|
||||||
{{lastUserSync | date:'medium'}}
|
<span *ngIf="!lastUserSync">-</span>
|
||||||
</p>
|
{{ lastUserSync | date: "medium" }}
|
||||||
<p>
|
</p>
|
||||||
{{'syncStatus' | i18n}}:
|
<p>
|
||||||
<strong *ngIf="syncRunning" class="text-success">{{'running' | i18n}}</strong>
|
{{ "syncStatus" | i18n }}:
|
||||||
<strong *ngIf="!syncRunning" class="text-danger">{{'stopped' | i18n}}</strong>
|
<strong *ngIf="syncRunning" class="text-success">{{ "running" | i18n }}</strong>
|
||||||
</p>
|
<strong *ngIf="!syncRunning" class="text-danger">{{ "stopped" | i18n }}</strong>
|
||||||
<form #startForm [appApiAction]="startPromise" class="d-inline">
|
</p>
|
||||||
<button (click)="start()" class="btn btn-primary"
|
<form #startForm [appApiAction]="startPromise" class="d-inline">
|
||||||
[disabled]="startForm.loading">
|
<button (click)="start()" class="btn btn-primary" [disabled]="startForm.loading">
|
||||||
<i class="fa fa-play fa-fw" [hidden]="startForm.loading"></i>
|
<i class="bwi bwi-play bwi-fw" [hidden]="startForm.loading"></i>
|
||||||
<i class="fa fa-spinner fa-fw fa-spin" [hidden]="!startForm.loading"></i>
|
<i class="bwi bwi-spinner bwi-fw bwi-spin" [hidden]="!startForm.loading"></i>
|
||||||
{{'startSync' | i18n}}
|
{{ "startSync" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<button (click)="stop()" class="btn btn-primary">
|
<button (click)="stop()" class="btn btn-primary">
|
||||||
<i class="fa fa-stop fa-fw"></i>
|
<i class="bwi bwi-stop bwi-fw"></i>
|
||||||
{{'stopSync' | i18n}}
|
{{ "stopSync" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
<form #syncForm [appApiAction]="syncPromise" class="d-inline">
|
<form #syncForm [appApiAction]="syncPromise" class="d-inline">
|
||||||
<button (click)="sync()" class="btn btn-primary"
|
<button (click)="sync()" class="btn btn-primary" [disabled]="syncForm.loading">
|
||||||
[disabled]="syncForm.loading">
|
<i class="bwi bwi-refresh bwi-fw" [ngClass]="{ 'bwi-spin': syncForm.loading }"></i>
|
||||||
<i class="fa fa-refresh fa-fw" [ngClass]="{'fa-spin': syncForm.loading}"></i>
|
{{ "syncNow" | i18n }}
|
||||||
{{'syncNow' | i18n}}
|
</button>
|
||||||
</button>
|
</form>
|
||||||
</form>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h3 class="card-header">{{'testing' | i18n}}</h3>
|
<h3 class="card-header">{{ "testing" | i18n }}</h3>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p>{{'testingDesc' | i18n}}</p>
|
<p>{{ "testingDesc" | i18n }}</p>
|
||||||
<form #simForm [appApiAction]="simPromise" class="d-inline">
|
<form #simForm [appApiAction]="simPromise" class="d-inline">
|
||||||
<button (click)="simulate()" class="btn btn-primary"
|
<button (click)="simulate()" class="btn btn-primary" [disabled]="simForm.loading">
|
||||||
[disabled]="simForm.loading">
|
<i class="bwi bwi-spinner bwi-fw bwi-spin" [hidden]="!simForm.loading"></i>
|
||||||
<i class="fa fa-spinner fa-fw fa-spin" [hidden]="!simForm.loading"></i>
|
<i class="bwi bwi-bug bwi-fw" [hidden]="simForm.loading"></i>
|
||||||
<i class="fa fa-bug fa-fw" [hidden]="simForm.loading"></i>
|
{{ "testNow" | i18n }}
|
||||||
{{'testNow' | i18n}}
|
</button>
|
||||||
</button>
|
</form>
|
||||||
</form>
|
<div class="form-check mt-2">
|
||||||
<div class="form-check mt-2">
|
<input
|
||||||
<input class="form-check-input" type="checkbox" id="simSinceLast" [(ngModel)]="simSinceLast">
|
class="form-check-input"
|
||||||
<label class="form-check-label" for="simSinceLast">{{'testLastSync' | i18n}}</label>
|
type="checkbox"
|
||||||
</div>
|
id="simSinceLast"
|
||||||
<ng-container *ngIf="!simForm.loading && (simUsers || simGroups)">
|
[(ngModel)]="simSinceLast"
|
||||||
<hr />
|
/>
|
||||||
<div class="row">
|
<label class="form-check-label" for="simSinceLast">{{ "testLastSync" | i18n }}</label>
|
||||||
<div class="col-lg">
|
|
||||||
<h4>{{'users' | i18n}}</h4>
|
|
||||||
<ul class="fa-ul testing-list" *ngIf="simEnabledUsers && simEnabledUsers.length">
|
|
||||||
<li *ngFor="let u of simEnabledUsers" title="{{u.referenceId}}">
|
|
||||||
<i class="fa-li fa fa-user"></i>
|
|
||||||
{{u.displayName}}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<p *ngIf="!simEnabledUsers || !simEnabledUsers.length">{{'noUsers' | i18n}}</p>
|
|
||||||
<h4>{{'disabledUsers' | i18n}}</h4>
|
|
||||||
<ul class="fa-ul testing-list" *ngIf="simDisabledUsers && simDisabledUsers.length">
|
|
||||||
<li *ngFor="let u of simDisabledUsers" title="{{u.referenceId}}">
|
|
||||||
<i class="fa-li fa fa-user"></i>
|
|
||||||
{{u.displayName}}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<p *ngIf="!simDisabledUsers || !simDisabledUsers.length">{{'noUsers' | i18n}}</p>
|
|
||||||
<h4>{{'deletedUsers' | i18n}}</h4>
|
|
||||||
<ul class="fa-ul testing-list" *ngIf="simDeletedUsers && simDeletedUsers.length">
|
|
||||||
<li *ngFor="let u of simDeletedUsers" title="{{u.referenceId}}">
|
|
||||||
<i class="fa-li fa fa-user"></i>
|
|
||||||
{{u.displayName}}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<p *ngIf="!simDeletedUsers || !simDeletedUsers.length">{{'noUsers' | i18n}}</p>
|
|
||||||
</div>
|
|
||||||
<div class="col-lg">
|
|
||||||
<h4>{{'groups' | i18n}}</h4>
|
|
||||||
<ul class="fa-ul testing-list" *ngIf="simGroups && simGroups.length">
|
|
||||||
<li *ngFor="let g of simGroups" title="{{g.referenceId}}">
|
|
||||||
<i class="fa-li fa fa-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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
</div>
|
</div>
|
||||||
|
<ng-container *ngIf="!simForm.loading && (simUsers || simGroups)">
|
||||||
|
<hr />
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg">
|
||||||
|
<h4>{{ "users" | i18n }}</h4>
|
||||||
|
<ul class="bwi-ul testing-list" *ngIf="simEnabledUsers && simEnabledUsers.length">
|
||||||
|
<li *ngFor="let u of simEnabledUsers" title="{{ u.referenceId }}">
|
||||||
|
<i class="bwi bwi-li bwi-user"></i>
|
||||||
|
{{ u.displayName }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p *ngIf="!simEnabledUsers || !simEnabledUsers.length">
|
||||||
|
{{ "noUsers" | i18n }}
|
||||||
|
</p>
|
||||||
|
<h4>{{ "disabledUsers" | i18n }}</h4>
|
||||||
|
<ul class="bwi-ul testing-list" *ngIf="simDisabledUsers && simDisabledUsers.length">
|
||||||
|
<li *ngFor="let u of simDisabledUsers" title="{{ u.referenceId }}">
|
||||||
|
<i class="bwi bwi-li bwi-user"></i>
|
||||||
|
{{ u.displayName }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p *ngIf="!simDisabledUsers || !simDisabledUsers.length">
|
||||||
|
{{ "noUsers" | i18n }}
|
||||||
|
</p>
|
||||||
|
<h4>{{ "deletedUsers" | i18n }}</h4>
|
||||||
|
<ul class="bwi-ul testing-list" *ngIf="simDeletedUsers && simDeletedUsers.length">
|
||||||
|
<li *ngFor="let u of simDeletedUsers" title="{{ u.referenceId }}">
|
||||||
|
<i class="bwi bwi-li bwi-user"></i>
|
||||||
|
{{ u.displayName }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p *ngIf="!simDeletedUsers || !simDeletedUsers.length">
|
||||||
|
{{ "noUsers" | i18n }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg">
|
||||||
|
<h4>{{ "groups" | i18n }}</h4>
|
||||||
|
<ul class="bwi-ul testing-list" *ngIf="simGroups && simGroups.length">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,123 +1,127 @@
|
|||||||
import {
|
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core";
|
||||||
ChangeDetectorRef,
|
|
||||||
Component,
|
|
||||||
NgZone,
|
|
||||||
OnDestroy,
|
|
||||||
OnInit,
|
|
||||||
} from '@angular/core';
|
|
||||||
|
|
||||||
import { ToasterService } from 'angular2-toaster';
|
import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service";
|
||||||
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
|
import { MessagingService } from "jslib-common/abstractions/messaging.service";
|
||||||
|
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
|
|
||||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
import { SyncService } from "../../services/sync.service";
|
||||||
import { MessagingService } from 'jslib-common/abstractions/messaging.service';
|
|
||||||
import { StateService } from 'jslib-common/abstractions/state.service';
|
|
||||||
|
|
||||||
import { SyncService } from '../../services/sync.service';
|
import { GroupEntry } from "../../models/groupEntry";
|
||||||
|
import { SimResult } from "../../models/simResult";
|
||||||
|
import { UserEntry } from "../../models/userEntry";
|
||||||
|
|
||||||
import { GroupEntry } from '../../models/groupEntry';
|
import { ConnectorUtils } from "../../utils";
|
||||||
import { SimResult } from '../../models/simResult';
|
|
||||||
import { UserEntry } from '../../models/userEntry';
|
|
||||||
import { ConfigurationService } from '../../services/configuration.service';
|
|
||||||
|
|
||||||
import { BroadcasterService } from 'jslib-angular/services/broadcaster.service';
|
import { StateService } from "../../abstractions/state.service";
|
||||||
|
|
||||||
import { ConnectorUtils } from '../../utils';
|
const BroadcasterSubscriptionId = "DashboardComponent";
|
||||||
|
|
||||||
const BroadcasterSubscriptionId = 'DashboardComponent';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-dashboard',
|
selector: "app-dashboard",
|
||||||
templateUrl: 'dashboard.component.html',
|
templateUrl: "dashboard.component.html",
|
||||||
})
|
})
|
||||||
export class DashboardComponent implements OnInit, OnDestroy {
|
export class DashboardComponent implements OnInit, OnDestroy {
|
||||||
simGroups: GroupEntry[];
|
simGroups: GroupEntry[];
|
||||||
simUsers: UserEntry[];
|
simUsers: UserEntry[];
|
||||||
simEnabledUsers: UserEntry[] = [];
|
simEnabledUsers: UserEntry[] = [];
|
||||||
simDisabledUsers: UserEntry[] = [];
|
simDisabledUsers: UserEntry[] = [];
|
||||||
simDeletedUsers: UserEntry[] = [];
|
simDeletedUsers: UserEntry[] = [];
|
||||||
simPromise: Promise<SimResult>;
|
simPromise: Promise<SimResult>;
|
||||||
simSinceLast: boolean = false;
|
simSinceLast: boolean = false;
|
||||||
syncPromise: Promise<[GroupEntry[], UserEntry[]]>;
|
syncPromise: Promise<[GroupEntry[], UserEntry[]]>;
|
||||||
startPromise: Promise<any>;
|
startPromise: Promise<any>;
|
||||||
lastGroupSync: Date;
|
lastGroupSync: Date;
|
||||||
lastUserSync: Date;
|
lastUserSync: Date;
|
||||||
syncRunning: boolean;
|
syncRunning: boolean;
|
||||||
|
|
||||||
constructor(private i18nService: I18nService, private syncService: SyncService,
|
constructor(
|
||||||
private configurationService: ConfigurationService, private broadcasterService: BroadcasterService,
|
private i18nService: I18nService,
|
||||||
private ngZone: NgZone, private messagingService: MessagingService,
|
private syncService: SyncService,
|
||||||
private toasterService: ToasterService, private changeDetectorRef: ChangeDetectorRef,
|
private broadcasterService: BroadcasterService,
|
||||||
private stateService: StateService) { }
|
private ngZone: NgZone,
|
||||||
|
private messagingService: MessagingService,
|
||||||
|
private platformUtilsService: PlatformUtilsService,
|
||||||
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
|
private stateService: StateService
|
||||||
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
|
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
|
||||||
this.ngZone.run(async () => {
|
this.ngZone.run(async () => {
|
||||||
switch (message.command) {
|
switch (message.command) {
|
||||||
case 'dirSyncCompleted':
|
case "dirSyncCompleted":
|
||||||
this.updateLastSync();
|
this.updateLastSync();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
this.changeDetectorRef.detectChanges();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
this.syncRunning = !!(await this.stateService.get('syncingDir'));
|
|
||||||
this.updateLastSync();
|
|
||||||
}
|
|
||||||
|
|
||||||
async ngOnDestroy() {
|
|
||||||
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async start() {
|
|
||||||
this.startPromise = this.syncService.sync(false, false);
|
|
||||||
await this.startPromise;
|
|
||||||
this.messagingService.send('scheduleNextDirSync');
|
|
||||||
this.syncRunning = true;
|
|
||||||
this.toasterService.popAsync('success', null, this.i18nService.t('syncingStarted'));
|
|
||||||
}
|
|
||||||
|
|
||||||
async stop() {
|
|
||||||
this.messagingService.send('cancelDirSync');
|
|
||||||
this.syncRunning = false;
|
|
||||||
this.toasterService.popAsync('success', null, this.i18nService.t('syncingStopped'));
|
|
||||||
}
|
|
||||||
|
|
||||||
async sync() {
|
|
||||||
this.syncPromise = this.syncService.sync(false, false);
|
|
||||||
const result = await this.syncPromise;
|
|
||||||
const groupCount = result[0] != null ? result[0].length : 0;
|
|
||||||
const userCount = result[1] != null ? result[1].length : 0;
|
|
||||||
this.toasterService.popAsync('success', null,
|
|
||||||
this.i18nService.t('syncCounts', groupCount.toString(), userCount.toString()));
|
|
||||||
}
|
|
||||||
|
|
||||||
async simulate() {
|
|
||||||
this.simGroups = [];
|
|
||||||
this.simUsers = [];
|
|
||||||
this.simEnabledUsers = [];
|
|
||||||
this.simDisabledUsers = [];
|
|
||||||
this.simDeletedUsers = [];
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.simPromise = ConnectorUtils.simulate(this.syncService, this.i18nService, this.simSinceLast);
|
|
||||||
const result = await this.simPromise;
|
|
||||||
this.simGroups = result.groups;
|
|
||||||
this.simUsers = result.users;
|
|
||||||
this.simEnabledUsers = result.enabledUsers;
|
|
||||||
this.simDisabledUsers = result.disabledUsers;
|
|
||||||
this.simDeletedUsers = result.deletedUsers;
|
|
||||||
} catch (e) {
|
|
||||||
this.simGroups = null;
|
|
||||||
this.simUsers = null;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private async updateLastSync() {
|
this.changeDetectorRef.detectChanges();
|
||||||
this.lastGroupSync = await this.configurationService.getLastGroupSyncDate();
|
});
|
||||||
this.lastUserSync = await this.configurationService.getLastUserSyncDate();
|
});
|
||||||
|
|
||||||
|
this.syncRunning = !!(await this.stateService.getSyncingDir());
|
||||||
|
this.updateLastSync();
|
||||||
|
}
|
||||||
|
|
||||||
|
async ngOnDestroy() {
|
||||||
|
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async start() {
|
||||||
|
this.startPromise = this.syncService.sync(false, false);
|
||||||
|
await this.startPromise;
|
||||||
|
this.messagingService.send("scheduleNextDirSync");
|
||||||
|
this.syncRunning = true;
|
||||||
|
this.platformUtilsService.showToast("success", null, this.i18nService.t("syncingStarted"));
|
||||||
|
}
|
||||||
|
|
||||||
|
async stop() {
|
||||||
|
this.messagingService.send("cancelDirSync");
|
||||||
|
this.syncRunning = false;
|
||||||
|
this.platformUtilsService.showToast("success", null, this.i18nService.t("syncingStopped"));
|
||||||
|
}
|
||||||
|
|
||||||
|
async sync() {
|
||||||
|
this.syncPromise = this.syncService.sync(false, false);
|
||||||
|
const result = await this.syncPromise;
|
||||||
|
const groupCount = result[0] != null ? result[0].length : 0;
|
||||||
|
const userCount = result[1] != null ? result[1].length : 0;
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"success",
|
||||||
|
null,
|
||||||
|
this.i18nService.t("syncCounts", groupCount.toString(), userCount.toString())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async simulate() {
|
||||||
|
this.simGroups = [];
|
||||||
|
this.simUsers = [];
|
||||||
|
this.simEnabledUsers = [];
|
||||||
|
this.simDisabledUsers = [];
|
||||||
|
this.simDeletedUsers = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.simPromise = ConnectorUtils.simulate(
|
||||||
|
this.syncService,
|
||||||
|
this.i18nService,
|
||||||
|
this.simSinceLast
|
||||||
|
);
|
||||||
|
const result = await this.simPromise;
|
||||||
|
this.simGroups = result.groups;
|
||||||
|
this.simUsers = result.users;
|
||||||
|
this.simEnabledUsers = result.enabledUsers;
|
||||||
|
this.simDisabledUsers = result.disabledUsers;
|
||||||
|
this.simDeletedUsers = result.deletedUsers;
|
||||||
|
} catch (e) {
|
||||||
|
this.simGroups = null;
|
||||||
|
this.simUsers = null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async updateLastSync() {
|
||||||
|
this.lastGroupSync = await this.stateService.getLastGroupSync();
|
||||||
|
this.lastUserSync = await this.stateService.getLastUserSync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,38 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h3 class="card-header">{{'about' | i18n}}</h3>
|
<h3 class="card-header">{{ "about" | i18n }}</h3>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p>
|
<p>
|
||||||
{{'bitwardenDirectoryConnector' | i18n}}
|
{{ "bitwardenDirectoryConnector" | i18n }}
|
||||||
<br /> {{'version' | i18n : version}}
|
<br />
|
||||||
<br /> © Bitwarden Inc. LLC 2015-{{year}}
|
{{ "version" | i18n: version }} <br />
|
||||||
</p>
|
© Bitwarden Inc. LLC 2015-{{ year }}
|
||||||
<button class="btn btn-primary" type="button" (click)="update()" [disabled]="checkingForUpdate">
|
</p>
|
||||||
<i class="fa fa-download fa-fw" [hidden]="checkingForUpdate"></i>
|
<button
|
||||||
<i class="fa fa-spinner fa-fw fa-spin" [hidden]="!checkingForUpdate"></i>
|
class="btn btn-primary"
|
||||||
{{'checkForUpdates' | i18n}}
|
type="button"
|
||||||
</button>
|
(click)="update()"
|
||||||
</div>
|
[disabled]="checkingForUpdate"
|
||||||
</div>
|
>
|
||||||
|
<i class="bwi bwi-download bwi-fw" [hidden]="checkingForUpdate"></i>
|
||||||
|
<i class="bwi bwi-spinner bwi-fw bwi-spin" [hidden]="!checkingForUpdate"></i>
|
||||||
|
{{ "checkForUpdates" | i18n }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm">
|
</div>
|
||||||
<div class="card">
|
<div class="col-sm">
|
||||||
<h3 class="card-header">{{'other' | i18n}}</h3>
|
<div class="card">
|
||||||
<div class="card-body">
|
<h3 class="card-header">{{ "other" | i18n }}</h3>
|
||||||
<button class="btn btn-primary" type="button" (click)="logOut()">
|
<div class="card-body">
|
||||||
{{'logOut' | i18n}}
|
<button class="btn btn-primary" type="button" (click)="logOut()">
|
||||||
</button>
|
{{ "logOut" | i18n }}
|
||||||
<button class="btn btn-primary" type="button" (click)="clearCache()">
|
</button>
|
||||||
{{'clearSyncCache' | i18n}}
|
<button class="btn btn-primary" type="button" (click)="clearCache()">
|
||||||
</button>
|
{{ "clearSyncCache" | i18n }}
|
||||||
</div>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,78 +1,77 @@
|
|||||||
import {
|
import { ChangeDetectorRef, Component, NgZone, OnInit } from "@angular/core";
|
||||||
ChangeDetectorRef,
|
|
||||||
Component,
|
|
||||||
NgZone,
|
|
||||||
OnDestroy,
|
|
||||||
OnInit,
|
|
||||||
} from '@angular/core';
|
|
||||||
|
|
||||||
import { ToasterService } from 'angular2-toaster';
|
import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service";
|
||||||
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
|
import { MessagingService } from "jslib-common/abstractions/messaging.service";
|
||||||
|
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
|
|
||||||
import { BroadcasterService } from 'jslib-angular/services/broadcaster.service';
|
import { StateService } from "../../abstractions/state.service";
|
||||||
|
|
||||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
const BroadcasterSubscriptionId = "MoreComponent";
|
||||||
import { MessagingService } from 'jslib-common/abstractions/messaging.service';
|
|
||||||
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
|
|
||||||
|
|
||||||
import { ConfigurationService } from '../../services/configuration.service';
|
|
||||||
|
|
||||||
const BroadcasterSubscriptionId = 'MoreComponent';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-more',
|
selector: "app-more",
|
||||||
templateUrl: 'more.component.html',
|
templateUrl: "more.component.html",
|
||||||
})
|
})
|
||||||
export class MoreComponent implements OnInit {
|
export class MoreComponent implements OnInit {
|
||||||
version: string;
|
version: string;
|
||||||
year: string;
|
year: string;
|
||||||
checkingForUpdate = false;
|
checkingForUpdate = false;
|
||||||
|
|
||||||
constructor(private platformUtilsService: PlatformUtilsService, private i18nService: I18nService,
|
constructor(
|
||||||
private messagingService: MessagingService, private configurationService: ConfigurationService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private toasterService: ToasterService, private broadcasterService: BroadcasterService,
|
private i18nService: I18nService,
|
||||||
private ngZone: NgZone, private changeDetectorRef: ChangeDetectorRef) { }
|
private messagingService: MessagingService,
|
||||||
|
private broadcasterService: BroadcasterService,
|
||||||
|
private ngZone: NgZone,
|
||||||
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
|
private stateService: StateService
|
||||||
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
|
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
|
||||||
this.ngZone.run(async () => {
|
this.ngZone.run(async () => {
|
||||||
switch (message.command) {
|
switch (message.command) {
|
||||||
case 'checkingForUpdate':
|
case "checkingForUpdate":
|
||||||
this.checkingForUpdate = true;
|
this.checkingForUpdate = true;
|
||||||
break;
|
break;
|
||||||
case 'doneCheckingForUpdate':
|
case "doneCheckingForUpdate":
|
||||||
this.checkingForUpdate = false;
|
this.checkingForUpdate = false;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
this.changeDetectorRef.detectChanges();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
this.year = new Date().getFullYear().toString();
|
|
||||||
this.version = await this.platformUtilsService.getApplicationVersion();
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy() {
|
|
||||||
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
update() {
|
|
||||||
this.messagingService.send('checkForUpdate');
|
|
||||||
}
|
|
||||||
|
|
||||||
async logOut() {
|
|
||||||
const confirmed = await this.platformUtilsService.showDialog(
|
|
||||||
this.i18nService.t('logOutConfirmation'), this.i18nService.t('logOut'),
|
|
||||||
this.i18nService.t('yes'), this.i18nService.t('cancel'));
|
|
||||||
if (confirmed) {
|
|
||||||
this.messagingService.send('logout');
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
async clearCache() {
|
this.changeDetectorRef.detectChanges();
|
||||||
await this.configurationService.clearStatefulSettings(true);
|
});
|
||||||
this.toasterService.popAsync('success', null, this.i18nService.t('syncCacheCleared'));
|
});
|
||||||
|
|
||||||
|
this.year = new Date().getFullYear().toString();
|
||||||
|
this.version = await this.platformUtilsService.getApplicationVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
update() {
|
||||||
|
this.messagingService.send("checkForUpdate");
|
||||||
|
}
|
||||||
|
|
||||||
|
async logOut() {
|
||||||
|
const confirmed = await this.platformUtilsService.showDialog(
|
||||||
|
this.i18nService.t("logOutConfirmation"),
|
||||||
|
this.i18nService.t("logOut"),
|
||||||
|
this.i18nService.t("yes"),
|
||||||
|
this.i18nService.t("cancel")
|
||||||
|
);
|
||||||
|
if (confirmed) {
|
||||||
|
this.messagingService.send("logout");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearCache() {
|
||||||
|
await this.stateService.clearSyncSettings(true);
|
||||||
|
this.platformUtilsService.showToast("success", null, this.i18nService.t("syncCacheCleared"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,153 +1,154 @@
|
|||||||
import {
|
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core";
|
||||||
ChangeDetectorRef,
|
|
||||||
Component,
|
|
||||||
NgZone,
|
|
||||||
OnDestroy,
|
|
||||||
OnInit,
|
|
||||||
} from '@angular/core';
|
|
||||||
|
|
||||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
import { StateService } from 'jslib-common/abstractions/state.service';
|
import { LogService } from "jslib-common/abstractions/log.service";
|
||||||
|
|
||||||
import { ProfileOrganizationResponse } from 'jslib-common/models/response/profileOrganizationResponse';
|
import { DirectoryType } from "../../enums/directoryType";
|
||||||
|
|
||||||
import { ConfigurationService } from '../../services/configuration.service';
|
import { AzureConfiguration } from "../../models/azureConfiguration";
|
||||||
|
import { GSuiteConfiguration } from "../../models/gsuiteConfiguration";
|
||||||
|
import { LdapConfiguration } from "../../models/ldapConfiguration";
|
||||||
|
import { OktaConfiguration } from "../../models/oktaConfiguration";
|
||||||
|
import { OneLoginConfiguration } from "../../models/oneLoginConfiguration";
|
||||||
|
import { SyncConfiguration } from "../../models/syncConfiguration";
|
||||||
|
|
||||||
import { DirectoryType } from '../../enums/directoryType';
|
import { StateService } from "../../abstractions/state.service";
|
||||||
|
import { ConnectorUtils } from "../../utils";
|
||||||
import { AzureConfiguration } from '../../models/azureConfiguration';
|
|
||||||
import { GSuiteConfiguration } from '../../models/gsuiteConfiguration';
|
|
||||||
import { LdapConfiguration } from '../../models/ldapConfiguration';
|
|
||||||
import { OktaConfiguration } from '../../models/oktaConfiguration';
|
|
||||||
import { OneLoginConfiguration } from '../../models/oneLoginConfiguration';
|
|
||||||
import { SyncConfiguration } from '../../models/syncConfiguration';
|
|
||||||
|
|
||||||
import { ConnectorUtils } from '../../utils';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-settings',
|
selector: "app-settings",
|
||||||
templateUrl: 'settings.component.html',
|
templateUrl: "settings.component.html",
|
||||||
})
|
})
|
||||||
export class SettingsComponent implements OnInit, OnDestroy {
|
export class SettingsComponent implements OnInit, OnDestroy {
|
||||||
directory: DirectoryType;
|
directory: DirectoryType;
|
||||||
directoryType = DirectoryType;
|
directoryType = DirectoryType;
|
||||||
ldap = new LdapConfiguration();
|
ldap = new LdapConfiguration();
|
||||||
gsuite = new GSuiteConfiguration();
|
gsuite = new GSuiteConfiguration();
|
||||||
azure = new AzureConfiguration();
|
azure = new AzureConfiguration();
|
||||||
okta = new OktaConfiguration();
|
okta = new OktaConfiguration();
|
||||||
oneLogin = new OneLoginConfiguration();
|
oneLogin = new OneLoginConfiguration();
|
||||||
sync = new SyncConfiguration();
|
sync = new SyncConfiguration();
|
||||||
directoryOptions: any[];
|
directoryOptions: any[];
|
||||||
showLdapPassword: boolean = false;
|
showLdapPassword: boolean = false;
|
||||||
showAzureKey: boolean = false;
|
showAzureKey: boolean = false;
|
||||||
showOktaKey: boolean = false;
|
showOktaKey: boolean = false;
|
||||||
showOneLoginSecret: boolean = false;
|
showOneLoginSecret: boolean = false;
|
||||||
|
|
||||||
constructor(private i18nService: I18nService, private configurationService: ConfigurationService,
|
constructor(
|
||||||
private changeDetectorRef: ChangeDetectorRef, private ngZone: NgZone,
|
private i18nService: I18nService,
|
||||||
private stateService: StateService) {
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
this.directoryOptions = [
|
private ngZone: NgZone,
|
||||||
{ name: i18nService.t('select'), value: null },
|
private logService: LogService,
|
||||||
{ name: 'Active Directory / LDAP', value: DirectoryType.Ldap },
|
private stateService: StateService
|
||||||
{ name: 'Azure Active Directory', value: DirectoryType.AzureActiveDirectory },
|
) {
|
||||||
{ name: 'G Suite (Google)', value: DirectoryType.GSuite },
|
this.directoryOptions = [
|
||||||
{ name: 'Okta', value: DirectoryType.Okta },
|
{ name: this.i18nService.t("select"), value: null },
|
||||||
{ name: 'OneLogin', value: DirectoryType.OneLogin },
|
{ name: "Active Directory / LDAP", value: DirectoryType.Ldap },
|
||||||
];
|
{ name: "Azure Active Directory", value: DirectoryType.AzureActiveDirectory },
|
||||||
|
{ name: "G Suite (Google)", value: DirectoryType.GSuite },
|
||||||
|
{ name: "Okta", value: DirectoryType.Okta },
|
||||||
|
{ name: "OneLogin", value: DirectoryType.OneLogin },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
this.directory = await this.stateService.getDirectoryType();
|
||||||
|
this.ldap =
|
||||||
|
(await this.stateService.getDirectory<LdapConfiguration>(DirectoryType.Ldap)) || this.ldap;
|
||||||
|
this.gsuite =
|
||||||
|
(await this.stateService.getDirectory<GSuiteConfiguration>(DirectoryType.GSuite)) ||
|
||||||
|
this.gsuite;
|
||||||
|
this.azure =
|
||||||
|
(await this.stateService.getDirectory<AzureConfiguration>(
|
||||||
|
DirectoryType.AzureActiveDirectory
|
||||||
|
)) || this.azure;
|
||||||
|
this.okta =
|
||||||
|
(await this.stateService.getDirectory<OktaConfiguration>(DirectoryType.Okta)) || this.okta;
|
||||||
|
this.oneLogin =
|
||||||
|
(await this.stateService.getDirectory<OneLoginConfiguration>(DirectoryType.OneLogin)) ||
|
||||||
|
this.oneLogin;
|
||||||
|
this.sync = (await this.stateService.getSync()) || this.sync;
|
||||||
|
}
|
||||||
|
|
||||||
|
async ngOnDestroy() {
|
||||||
|
await this.submit();
|
||||||
|
}
|
||||||
|
|
||||||
|
async submit() {
|
||||||
|
ConnectorUtils.adjustConfigForSave(this.ldap, this.sync);
|
||||||
|
if (this.ldap != null && this.ldap.ad) {
|
||||||
|
this.ldap.pagedSearch = true;
|
||||||
|
}
|
||||||
|
await this.stateService.setDirectoryType(this.directory);
|
||||||
|
await this.stateService.setDirectory(DirectoryType.Ldap, this.ldap);
|
||||||
|
await this.stateService.setDirectory(DirectoryType.GSuite, this.gsuite);
|
||||||
|
await this.stateService.setDirectory(DirectoryType.AzureActiveDirectory, this.azure);
|
||||||
|
await this.stateService.setDirectory(DirectoryType.Okta, this.okta);
|
||||||
|
await this.stateService.setDirectory(DirectoryType.OneLogin, this.oneLogin);
|
||||||
|
await this.stateService.setSync(this.sync);
|
||||||
|
}
|
||||||
|
|
||||||
|
parseKeyFile() {
|
||||||
|
const filePicker = document.getElementById("keyFile") as HTMLInputElement;
|
||||||
|
if (filePicker.files == null || filePicker.files.length < 0) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
const reader = new FileReader();
|
||||||
this.directory = await this.configurationService.getDirectoryType();
|
reader.readAsText(filePicker.files[0], "utf-8");
|
||||||
this.ldap = (await this.configurationService.getDirectory<LdapConfiguration>(DirectoryType.Ldap)) ||
|
reader.onload = (evt) => {
|
||||||
this.ldap;
|
this.ngZone.run(async () => {
|
||||||
this.gsuite = (await this.configurationService.getDirectory<GSuiteConfiguration>(DirectoryType.GSuite)) ||
|
try {
|
||||||
this.gsuite;
|
const result = JSON.parse((evt.target as FileReader).result as string);
|
||||||
this.azure = (await this.configurationService.getDirectory<AzureConfiguration>(
|
if (result.client_email != null && result.private_key != null) {
|
||||||
DirectoryType.AzureActiveDirectory)) || this.azure;
|
this.gsuite.clientEmail = result.client_email;
|
||||||
this.okta = (await this.configurationService.getDirectory<OktaConfiguration>(
|
this.gsuite.privateKey = result.private_key;
|
||||||
DirectoryType.Okta)) || this.okta;
|
}
|
||||||
this.oneLogin = (await this.configurationService.getDirectory<OneLoginConfiguration>(
|
} catch (e) {
|
||||||
DirectoryType.OneLogin)) || this.oneLogin;
|
this.logService.error(e);
|
||||||
this.sync = (await this.configurationService.getSync()) || this.sync;
|
|
||||||
}
|
|
||||||
|
|
||||||
async ngOnDestroy() {
|
|
||||||
await this.submit();
|
|
||||||
}
|
|
||||||
|
|
||||||
async submit() {
|
|
||||||
ConnectorUtils.adjustConfigForSave(this.ldap, this.sync);
|
|
||||||
if (this.ldap != null && this.ldap.ad) {
|
|
||||||
this.ldap.pagedSearch = true;
|
|
||||||
}
|
}
|
||||||
await this.configurationService.saveDirectoryType(this.directory);
|
this.changeDetectorRef.detectChanges();
|
||||||
await this.configurationService.saveDirectory(DirectoryType.Ldap, this.ldap);
|
});
|
||||||
await this.configurationService.saveDirectory(DirectoryType.GSuite, this.gsuite);
|
|
||||||
await this.configurationService.saveDirectory(DirectoryType.AzureActiveDirectory, this.azure);
|
// reset file input
|
||||||
await this.configurationService.saveDirectory(DirectoryType.Okta, this.okta);
|
// ref: https://stackoverflow.com/a/20552042
|
||||||
await this.configurationService.saveDirectory(DirectoryType.OneLogin, this.oneLogin);
|
filePicker.type = "";
|
||||||
await this.configurationService.saveSync(this.sync);
|
filePicker.type = "file";
|
||||||
|
filePicker.value = "";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
setSslPath(id: string) {
|
||||||
|
const filePicker = document.getElementById(id + "_file") as HTMLInputElement;
|
||||||
|
if (filePicker.files == null || filePicker.files.length < 0) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
parseKeyFile() {
|
(this.ldap as any)[id] = filePicker.files[0].path;
|
||||||
const filePicker = (document.getElementById('keyFile') as HTMLInputElement);
|
// reset file input
|
||||||
if (filePicker.files == null || filePicker.files.length < 0) {
|
// ref: https://stackoverflow.com/a/20552042
|
||||||
return;
|
filePicker.type = "";
|
||||||
}
|
filePicker.type = "file";
|
||||||
|
filePicker.value = "";
|
||||||
|
}
|
||||||
|
|
||||||
const reader = new FileReader();
|
toggleLdapPassword() {
|
||||||
reader.readAsText(filePicker.files[0], 'utf-8');
|
this.showLdapPassword = !this.showLdapPassword;
|
||||||
reader.onload = evt => {
|
document.getElementById("password").focus();
|
||||||
this.ngZone.run(async () => {
|
}
|
||||||
try {
|
|
||||||
const result = JSON.parse((evt.target as FileReader).result as string);
|
|
||||||
if (result.client_email != null && result.private_key != null) {
|
|
||||||
this.gsuite.clientEmail = result.client_email;
|
|
||||||
this.gsuite.privateKey = result.private_key;
|
|
||||||
}
|
|
||||||
} catch { }
|
|
||||||
this.changeDetectorRef.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
// reset file input
|
toggleAzureKey() {
|
||||||
// ref: https://stackoverflow.com/a/20552042
|
this.showAzureKey = !this.showAzureKey;
|
||||||
filePicker.type = '';
|
document.getElementById("secretKey").focus();
|
||||||
filePicker.type = 'file';
|
}
|
||||||
filePicker.value = '';
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
setSslPath(id: string) {
|
toggleOktaKey() {
|
||||||
const filePicker = (document.getElementById(id + '_file') as HTMLInputElement);
|
this.showOktaKey = !this.showOktaKey;
|
||||||
if (filePicker.files == null || filePicker.files.length < 0) {
|
document.getElementById("oktaToken").focus();
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
(this.ldap as any)[id] = filePicker.files[0].path;
|
toggleOneLoginSecret() {
|
||||||
// reset file input
|
this.showOneLoginSecret = !this.showOneLoginSecret;
|
||||||
// ref: https://stackoverflow.com/a/20552042
|
document.getElementById("oneLoginClientSecret").focus();
|
||||||
filePicker.type = '';
|
}
|
||||||
filePicker.type = 'file';
|
|
||||||
filePicker.value = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleLdapPassword() {
|
|
||||||
this.showLdapPassword = !this.showLdapPassword;
|
|
||||||
document.getElementById('password').focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleAzureKey() {
|
|
||||||
this.showAzureKey = !this.showAzureKey;
|
|
||||||
document.getElementById('secretKey').focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleOktaKey() {
|
|
||||||
this.showOktaKey = !this.showOktaKey;
|
|
||||||
document.getElementById('oktaToken').focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleOneLoginSecret() {
|
|
||||||
this.showOneLoginSecret = !this.showOneLoginSecret;
|
|
||||||
document.getElementById('oneLoginClientSecret').focus();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<ul class="nav nav-tabs mb-3">
|
<ul class="nav nav-tabs mb-3">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" routerLink="dashboard" routerLinkActive="active">
|
<a class="nav-link" routerLink="dashboard" routerLinkActive="active">
|
||||||
<i class="fa fa-dashboard"></i>
|
<i class="bwi bwi-dashboard"></i>
|
||||||
{{'dashboard' | i18n}}
|
{{ "dashboard" | i18n }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" routerLink="settings" routerLinkActive="active">
|
<a class="nav-link" routerLink="settings" routerLinkActive="active">
|
||||||
<i class="fa fa-cogs"></i>
|
<i class="bwi bwi-cogs"></i>
|
||||||
{{'settings' | i18n}}
|
{{ "settings" | i18n }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" routerLink="more" routerLinkActive="active">
|
<a class="nav-link" routerLink="more" routerLinkActive="active">
|
||||||
<i class="fa fa-sliders"></i>
|
<i class="bwi bwi-sliders"></i>
|
||||||
{{'more' | i18n}}
|
{{ "more" | i18n }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<router-outlet></router-outlet>
|
<router-outlet></router-outlet>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from "@angular/core";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-tabs',
|
selector: "app-tabs",
|
||||||
templateUrl: 'tabs.component.html',
|
templateUrl: "tabs.component.html",
|
||||||
})
|
})
|
||||||
export class TabsComponent { }
|
export class TabsComponent {}
|
||||||
|
|||||||
399
src/bwdc.ts
399
src/bwdc.ts
@@ -1,147 +1,294 @@
|
|||||||
import * as fs from 'fs';
|
import * as fs from "fs";
|
||||||
import * as path from 'path';
|
import * as path from "path";
|
||||||
|
|
||||||
import { LogLevelType } from 'jslib-common/enums/logLevelType';
|
import { LogLevelType } from "jslib-common/enums/logLevelType";
|
||||||
|
|
||||||
import { AuthService } from './services/auth.service';
|
import { AuthService } from "./services/auth.service";
|
||||||
|
|
||||||
import { ConfigurationService } from './services/configuration.service';
|
import { I18nService } from "./services/i18n.service";
|
||||||
import { I18nService } from './services/i18n.service';
|
import { KeytarSecureStorageService } from "./services/keytarSecureStorage.service";
|
||||||
import { KeytarSecureStorageService } from './services/keytarSecureStorage.service';
|
import { LowdbStorageService } from "./services/lowdbStorage.service";
|
||||||
import { LowdbStorageService } from './services/lowdbStorage.service';
|
import { StateService } from "./services/state.service";
|
||||||
import { NodeApiService } from './services/nodeApi.service';
|
import { StateMigrationService } from "./services/stateMigration.service";
|
||||||
import { SyncService } from './services/sync.service';
|
import { SyncService } from "./services/sync.service";
|
||||||
|
|
||||||
import { CliPlatformUtilsService } from 'jslib-node/cli/services/cliPlatformUtils.service';
|
import { CliPlatformUtilsService } from "jslib-node/cli/services/cliPlatformUtils.service";
|
||||||
import { ConsoleLogService } from 'jslib-node/cli/services/consoleLog.service';
|
import { ConsoleLogService } from "jslib-node/cli/services/consoleLog.service";
|
||||||
import { NodeCryptoFunctionService } from 'jslib-node/services/nodeCryptoFunction.service';
|
import { NodeApiService } from "jslib-node/services/nodeApi.service";
|
||||||
|
import { NodeCryptoFunctionService } from "jslib-node/services/nodeCryptoFunction.service";
|
||||||
|
|
||||||
import { ApiKeyService } from 'jslib-common/services/apiKey.service';
|
import { AppIdService } from "jslib-common/services/appId.service";
|
||||||
import { AppIdService } from 'jslib-common/services/appId.service';
|
import { CipherService } from "jslib-common/services/cipher.service";
|
||||||
import { ConstantsService } from 'jslib-common/services/constants.service';
|
import { CollectionService } from "jslib-common/services/collection.service";
|
||||||
import { ContainerService } from 'jslib-common/services/container.service';
|
import { ContainerService } from "jslib-common/services/container.service";
|
||||||
import { CryptoService } from 'jslib-common/services/crypto.service';
|
import { CryptoService } from "jslib-common/services/crypto.service";
|
||||||
import { EnvironmentService } from 'jslib-common/services/environment.service';
|
import { EnvironmentService } from "jslib-common/services/environment.service";
|
||||||
import { NoopMessagingService } from 'jslib-common/services/noopMessaging.service';
|
import { FileUploadService } from "jslib-common/services/fileUpload.service";
|
||||||
import { PasswordGenerationService } from 'jslib-common/services/passwordGeneration.service';
|
import { FolderService } from "jslib-common/services/folder.service";
|
||||||
import { TokenService } from 'jslib-common/services/token.service';
|
import { KeyConnectorService } from "jslib-common/services/keyConnector.service";
|
||||||
import { UserService } from 'jslib-common/services/user.service';
|
import { NoopMessagingService } from "jslib-common/services/noopMessaging.service";
|
||||||
|
import { OrganizationService } from "jslib-common/services/organization.service";
|
||||||
|
import { PasswordGenerationService } from "jslib-common/services/passwordGeneration.service";
|
||||||
|
import { PolicyService } from "jslib-common/services/policy.service";
|
||||||
|
import { ProviderService } from "jslib-common/services/provider.service";
|
||||||
|
import { SearchService } from "jslib-common/services/search.service";
|
||||||
|
import { SendService } from "jslib-common/services/send.service";
|
||||||
|
import { SettingsService } from "jslib-common/services/settings.service";
|
||||||
|
import { TokenService } from "jslib-common/services/token.service";
|
||||||
|
|
||||||
import { StorageService as StorageServiceAbstraction } from 'jslib-common/abstractions/storage.service';
|
import { StorageService as StorageServiceAbstraction } from "jslib-common/abstractions/storage.service";
|
||||||
|
|
||||||
import { Program } from './program';
|
import { Program } from "./program";
|
||||||
import { refreshToken } from './services/api.service';
|
|
||||||
|
import { Account } from "./models/account";
|
||||||
|
|
||||||
|
import { GlobalStateFactory } from "jslib-common/factories/globalStateFactory";
|
||||||
|
import { StateFactory } from "jslib-common/factories/stateFactory";
|
||||||
|
|
||||||
|
import { GlobalState } from "jslib-common/models/domain/globalState";
|
||||||
|
|
||||||
// tslint:disable-next-line
|
// tslint:disable-next-line
|
||||||
const packageJson = require('./package.json');
|
const packageJson = require("./package.json");
|
||||||
|
|
||||||
|
export let searchService: SearchService = null;
|
||||||
export class Main {
|
export class Main {
|
||||||
dataFilePath: string;
|
dataFilePath: string;
|
||||||
logService: ConsoleLogService;
|
logService: ConsoleLogService;
|
||||||
messagingService: NoopMessagingService;
|
messagingService: NoopMessagingService;
|
||||||
storageService: LowdbStorageService;
|
storageService: LowdbStorageService;
|
||||||
secureStorageService: StorageServiceAbstraction;
|
secureStorageService: StorageServiceAbstraction;
|
||||||
i18nService: I18nService;
|
i18nService: I18nService;
|
||||||
platformUtilsService: CliPlatformUtilsService;
|
platformUtilsService: CliPlatformUtilsService;
|
||||||
constantsService: ConstantsService;
|
cryptoService: CryptoService;
|
||||||
cryptoService: CryptoService;
|
tokenService: TokenService;
|
||||||
tokenService: TokenService;
|
appIdService: AppIdService;
|
||||||
appIdService: AppIdService;
|
apiService: NodeApiService;
|
||||||
apiService: NodeApiService;
|
environmentService: EnvironmentService;
|
||||||
environmentService: EnvironmentService;
|
containerService: ContainerService;
|
||||||
apiKeyService: ApiKeyService;
|
cryptoFunctionService: NodeCryptoFunctionService;
|
||||||
userService: UserService;
|
authService: AuthService;
|
||||||
containerService: ContainerService;
|
collectionService: CollectionService;
|
||||||
cryptoFunctionService: NodeCryptoFunctionService;
|
cipherService: CipherService;
|
||||||
authService: AuthService;
|
fileUploadService: FileUploadService;
|
||||||
configurationService: ConfigurationService;
|
folderService: FolderService;
|
||||||
syncService: SyncService;
|
searchService: SearchService;
|
||||||
passwordGenerationService: PasswordGenerationService;
|
sendService: SendService;
|
||||||
program: Program;
|
settingsService: SettingsService;
|
||||||
|
syncService: SyncService;
|
||||||
|
passwordGenerationService: PasswordGenerationService;
|
||||||
|
policyService: PolicyService;
|
||||||
|
keyConnectorService: KeyConnectorService;
|
||||||
|
program: Program;
|
||||||
|
stateService: StateService;
|
||||||
|
stateMigrationService: StateMigrationService;
|
||||||
|
organizationService: OrganizationService;
|
||||||
|
providerService: ProviderService;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
const applicationName = 'Bitwarden Directory Connector';
|
const applicationName = "Bitwarden Directory Connector";
|
||||||
if (process.env.BITWARDENCLI_CONNECTOR_APPDATA_DIR) {
|
if (process.env.BITWARDENCLI_CONNECTOR_APPDATA_DIR) {
|
||||||
this.dataFilePath = path.resolve(process.env.BITWARDENCLI_CONNECTOR_APPDATA_DIR);
|
this.dataFilePath = path.resolve(process.env.BITWARDENCLI_CONNECTOR_APPDATA_DIR);
|
||||||
} else if (process.env.BITWARDEN_CONNECTOR_APPDATA_DIR) {
|
} else if (process.env.BITWARDEN_CONNECTOR_APPDATA_DIR) {
|
||||||
this.dataFilePath = path.resolve(process.env.BITWARDEN_CONNECTOR_APPDATA_DIR);
|
this.dataFilePath = path.resolve(process.env.BITWARDEN_CONNECTOR_APPDATA_DIR);
|
||||||
} else if (fs.existsSync(path.join(__dirname, 'bitwarden-connector-appdata'))) {
|
} else if (fs.existsSync(path.join(__dirname, "bitwarden-connector-appdata"))) {
|
||||||
this.dataFilePath = path.join(__dirname, 'bitwarden-connector-appdata');
|
this.dataFilePath = path.join(__dirname, "bitwarden-connector-appdata");
|
||||||
} else if (process.platform === 'darwin') {
|
} else if (process.platform === "darwin") {
|
||||||
this.dataFilePath = path.join(process.env.HOME, 'Library/Application Support/' + applicationName);
|
this.dataFilePath = path.join(
|
||||||
} else if (process.platform === 'win32') {
|
process.env.HOME,
|
||||||
this.dataFilePath = path.join(process.env.APPDATA, applicationName);
|
"Library/Application Support/" + applicationName
|
||||||
} else if (process.env.XDG_CONFIG_HOME) {
|
);
|
||||||
this.dataFilePath = path.join(process.env.XDG_CONFIG_HOME, applicationName);
|
} else if (process.platform === "win32") {
|
||||||
} else {
|
this.dataFilePath = path.join(process.env.APPDATA, applicationName);
|
||||||
this.dataFilePath = path.join(process.env.HOME, '.config/' + applicationName);
|
} else if (process.env.XDG_CONFIG_HOME) {
|
||||||
}
|
this.dataFilePath = path.join(process.env.XDG_CONFIG_HOME, applicationName);
|
||||||
|
} else {
|
||||||
const plaintextSecrets = process.env.BITWARDENCLI_CONNECTOR_PLAINTEXT_SECRETS === 'true';
|
this.dataFilePath = path.join(process.env.HOME, ".config/" + applicationName);
|
||||||
this.i18nService = new I18nService('en', './locales');
|
|
||||||
this.platformUtilsService = new CliPlatformUtilsService('connector', packageJson);
|
|
||||||
this.logService = new ConsoleLogService(this.platformUtilsService.isDev(),
|
|
||||||
level => process.env.BITWARDENCLI_CONNECTOR_DEBUG !== 'true' && level <= LogLevelType.Info);
|
|
||||||
this.cryptoFunctionService = new NodeCryptoFunctionService();
|
|
||||||
this.storageService = new LowdbStorageService(this.logService, null, this.dataFilePath, false, true);
|
|
||||||
this.secureStorageService = plaintextSecrets ?
|
|
||||||
this.storageService : new KeytarSecureStorageService(applicationName);
|
|
||||||
this.cryptoService = new CryptoService(this.storageService, this.secureStorageService,
|
|
||||||
this.cryptoFunctionService, this.platformUtilsService, this.logService);
|
|
||||||
this.appIdService = new AppIdService(this.storageService);
|
|
||||||
this.tokenService = new TokenService(this.storageService);
|
|
||||||
this.messagingService = new NoopMessagingService();
|
|
||||||
this.environmentService = new EnvironmentService(this.storageService);
|
|
||||||
this.apiService = new NodeApiService(this.tokenService, this.platformUtilsService, this.environmentService,
|
|
||||||
() => refreshToken(this.apiKeyService, this.authService), async (expired: boolean) => await this.logout(),
|
|
||||||
'Bitwarden_DC/' + this.platformUtilsService.getApplicationVersion() +
|
|
||||||
' (' + this.platformUtilsService.getDeviceString().toUpperCase() + ')', (clientId, clientSecret) =>
|
|
||||||
this.authService.logInApiKey(clientId, clientSecret));
|
|
||||||
this.apiKeyService = new ApiKeyService(this.tokenService, this.storageService);
|
|
||||||
this.userService = new UserService(this.tokenService, this.storageService);
|
|
||||||
this.containerService = new ContainerService(this.cryptoService);
|
|
||||||
this.authService = new AuthService(this.cryptoService, this.apiService, this.userService, this.tokenService,
|
|
||||||
this.appIdService, this.i18nService, this.platformUtilsService, this.messagingService, null,
|
|
||||||
this.logService, this.apiKeyService, false);
|
|
||||||
this.configurationService = new ConfigurationService(this.storageService, this.secureStorageService,
|
|
||||||
process.env.BITWARDENCLI_CONNECTOR_PLAINTEXT_SECRETS !== 'true');
|
|
||||||
this.syncService = new SyncService(this.configurationService, this.logService, this.cryptoFunctionService,
|
|
||||||
this.apiService, this.messagingService, this.i18nService, this.environmentService);
|
|
||||||
this.passwordGenerationService = new PasswordGenerationService(this.cryptoService, this.storageService, null);
|
|
||||||
this.program = new Program(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async run() {
|
const plaintextSecrets = process.env.BITWARDENCLI_CONNECTOR_PLAINTEXT_SECRETS === "true";
|
||||||
await this.init();
|
this.i18nService = new I18nService("en", "./locales");
|
||||||
this.program.run();
|
this.platformUtilsService = new CliPlatformUtilsService("connector", packageJson);
|
||||||
}
|
this.logService = new ConsoleLogService(
|
||||||
|
this.platformUtilsService.isDev(),
|
||||||
|
(level) => process.env.BITWARDENCLI_CONNECTOR_DEBUG !== "true" && level <= LogLevelType.Info
|
||||||
|
);
|
||||||
|
this.cryptoFunctionService = new NodeCryptoFunctionService();
|
||||||
|
this.storageService = new LowdbStorageService(
|
||||||
|
this.logService,
|
||||||
|
null,
|
||||||
|
this.dataFilePath,
|
||||||
|
false,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
this.secureStorageService = plaintextSecrets
|
||||||
|
? this.storageService
|
||||||
|
: new KeytarSecureStorageService(applicationName);
|
||||||
|
|
||||||
async logout() {
|
this.stateMigrationService = new StateMigrationService(
|
||||||
await this.tokenService.clearToken();
|
this.storageService,
|
||||||
await this.apiKeyService.clear();
|
this.secureStorageService,
|
||||||
}
|
new StateFactory(GlobalState, Account)
|
||||||
|
);
|
||||||
|
|
||||||
private async init() {
|
this.stateService = new StateService(
|
||||||
await this.storageService.init();
|
this.storageService,
|
||||||
await this.apiKeyService.migrateApiKeyStorage();
|
this.secureStorageService,
|
||||||
this.containerService.attachToWindow(global);
|
this.logService,
|
||||||
await this.environmentService.setUrlsFromStorage();
|
this.stateMigrationService,
|
||||||
// Dev Server URLs. Comment out the line above.
|
process.env.BITWARDENCLI_CONNECTOR_PLAINTEXT_SECRETS !== "true",
|
||||||
// this.apiService.setUrls({
|
new StateFactory(GlobalState, Account)
|
||||||
// base: null,
|
);
|
||||||
// api: 'http://localhost:4000',
|
|
||||||
// identity: 'http://localhost:33656',
|
|
||||||
// });
|
|
||||||
const locale = await this.storageService.get<string>(ConstantsService.localeKey);
|
|
||||||
await this.i18nService.init(locale);
|
|
||||||
this.authService.init();
|
|
||||||
|
|
||||||
const installedVersion = await this.storageService.get<string>(ConstantsService.installedVersionKey);
|
this.cryptoService = new CryptoService(
|
||||||
const currentVersion = await this.platformUtilsService.getApplicationVersion();
|
this.cryptoFunctionService,
|
||||||
if (installedVersion == null || installedVersion !== currentVersion) {
|
this.platformUtilsService,
|
||||||
await this.storageService.save(ConstantsService.installedVersionKey, currentVersion);
|
this.logService,
|
||||||
}
|
this.stateService
|
||||||
|
);
|
||||||
|
|
||||||
|
this.appIdService = new AppIdService(this.storageService);
|
||||||
|
this.tokenService = new TokenService(this.stateService);
|
||||||
|
this.messagingService = new NoopMessagingService();
|
||||||
|
this.environmentService = new EnvironmentService(this.stateService);
|
||||||
|
this.apiService = new NodeApiService(
|
||||||
|
this.tokenService,
|
||||||
|
this.platformUtilsService,
|
||||||
|
this.environmentService,
|
||||||
|
async (expired: boolean) => await this.logout(),
|
||||||
|
"Bitwarden_DC/" +
|
||||||
|
this.platformUtilsService.getApplicationVersion() +
|
||||||
|
" (" +
|
||||||
|
this.platformUtilsService.getDeviceString().toUpperCase() +
|
||||||
|
")",
|
||||||
|
(clientId, clientSecret) => this.authService.logInApiKey(clientId, clientSecret)
|
||||||
|
);
|
||||||
|
this.containerService = new ContainerService(this.cryptoService);
|
||||||
|
|
||||||
|
this.organizationService = new OrganizationService(this.stateService);
|
||||||
|
|
||||||
|
this.keyConnectorService = new KeyConnectorService(
|
||||||
|
this.stateService,
|
||||||
|
this.cryptoService,
|
||||||
|
this.apiService,
|
||||||
|
this.tokenService,
|
||||||
|
this.logService,
|
||||||
|
this.organizationService
|
||||||
|
);
|
||||||
|
|
||||||
|
this.authService = new AuthService(
|
||||||
|
this.cryptoService,
|
||||||
|
this.apiService,
|
||||||
|
this.tokenService,
|
||||||
|
this.appIdService,
|
||||||
|
this.i18nService,
|
||||||
|
this.platformUtilsService,
|
||||||
|
this.messagingService,
|
||||||
|
null,
|
||||||
|
this.logService,
|
||||||
|
this.cryptoFunctionService,
|
||||||
|
this.environmentService,
|
||||||
|
this.keyConnectorService,
|
||||||
|
this.stateService
|
||||||
|
);
|
||||||
|
|
||||||
|
this.syncService = new SyncService(
|
||||||
|
this.logService,
|
||||||
|
this.cryptoFunctionService,
|
||||||
|
this.apiService,
|
||||||
|
this.messagingService,
|
||||||
|
this.i18nService,
|
||||||
|
this.environmentService,
|
||||||
|
this.stateService
|
||||||
|
);
|
||||||
|
|
||||||
|
this.policyService = new PolicyService(
|
||||||
|
this.stateService,
|
||||||
|
this.organizationService,
|
||||||
|
this.apiService
|
||||||
|
);
|
||||||
|
|
||||||
|
this.passwordGenerationService = new PasswordGenerationService(
|
||||||
|
this.cryptoService,
|
||||||
|
this.policyService,
|
||||||
|
this.stateService
|
||||||
|
);
|
||||||
|
|
||||||
|
this.settingsService = new SettingsService(this.stateService);
|
||||||
|
|
||||||
|
this.fileUploadService = new FileUploadService(this.logService, this.apiService);
|
||||||
|
|
||||||
|
this.cipherService = new CipherService(
|
||||||
|
this.cryptoService,
|
||||||
|
this.settingsService,
|
||||||
|
this.apiService,
|
||||||
|
this.fileUploadService,
|
||||||
|
this.i18nService,
|
||||||
|
() => searchService,
|
||||||
|
this.logService,
|
||||||
|
this.stateService
|
||||||
|
);
|
||||||
|
|
||||||
|
this.searchService = new SearchService(this.cipherService, this.logService, this.i18nService);
|
||||||
|
|
||||||
|
this.folderService = new FolderService(
|
||||||
|
this.cryptoService,
|
||||||
|
this.apiService,
|
||||||
|
this.i18nService,
|
||||||
|
this.cipherService,
|
||||||
|
this.stateService
|
||||||
|
);
|
||||||
|
|
||||||
|
this.collectionService = new CollectionService(
|
||||||
|
this.cryptoService,
|
||||||
|
this.i18nService,
|
||||||
|
this.stateService
|
||||||
|
);
|
||||||
|
|
||||||
|
this.sendService = new SendService(
|
||||||
|
this.cryptoService,
|
||||||
|
this.apiService,
|
||||||
|
this.fileUploadService,
|
||||||
|
this.i18nService,
|
||||||
|
this.cryptoFunctionService,
|
||||||
|
this.stateService
|
||||||
|
);
|
||||||
|
|
||||||
|
this.providerService = new ProviderService(this.stateService);
|
||||||
|
|
||||||
|
this.program = new Program(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
await this.init();
|
||||||
|
this.program.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
async logout() {
|
||||||
|
await this.tokenService.clearToken();
|
||||||
|
await this.stateService.clean();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async init() {
|
||||||
|
await this.storageService.init();
|
||||||
|
await this.stateService.init();
|
||||||
|
this.containerService.attachToWindow(global);
|
||||||
|
await this.environmentService.setUrlsFromStorage();
|
||||||
|
// Dev Server URLs. Comment out the line above.
|
||||||
|
// this.apiService.setUrls({
|
||||||
|
// base: null,
|
||||||
|
// api: 'http://localhost:4000',
|
||||||
|
// identity: 'http://localhost:33656',
|
||||||
|
// });
|
||||||
|
const locale = await this.stateService.getLocale();
|
||||||
|
await this.i18nService.init(locale);
|
||||||
|
this.authService.init();
|
||||||
|
|
||||||
|
const installedVersion = await this.stateService.getInstalledVersion();
|
||||||
|
const currentVersion = await this.platformUtilsService.getApplicationVersion();
|
||||||
|
if (installedVersion == null || installedVersion !== currentVersion) {
|
||||||
|
await this.stateService.setInstalledVersion(currentVersion);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const main = new Main();
|
const main = new Main();
|
||||||
|
|||||||
@@ -1,22 +1,21 @@
|
|||||||
import * as program from 'commander';
|
import * as program from "commander";
|
||||||
|
|
||||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
|
|
||||||
import { ConfigurationService } from '../services/configuration.service';
|
import { Response } from "jslib-node/cli/models/response";
|
||||||
|
import { MessageResponse } from "jslib-node/cli/models/response/messageResponse";
|
||||||
import { Response } from 'jslib-node/cli/models/response';
|
import { StateService } from "../abstractions/state.service";
|
||||||
import { MessageResponse } from 'jslib-node/cli/models/response/messageResponse';
|
|
||||||
|
|
||||||
export class ClearCacheCommand {
|
export class ClearCacheCommand {
|
||||||
constructor(private configurationService: ConfigurationService, private i18nService: I18nService) { }
|
constructor(private i18nService: I18nService, private stateService: StateService) {}
|
||||||
|
|
||||||
async run(cmd: program.OptionValues): Promise<Response> {
|
async run(cmd: program.OptionValues): Promise<Response> {
|
||||||
try {
|
try {
|
||||||
await this.configurationService.clearStatefulSettings(true);
|
await this.stateService.clearSyncSettings(true);
|
||||||
const res = new MessageResponse(this.i18nService.t('syncCacheCleared'), null);
|
const res = new MessageResponse(this.i18nService.t("syncCacheCleared"), null);
|
||||||
return Response.success(res);
|
return Response.success(res);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return Response.error(e);
|
return Response.error(e);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,150 +1,157 @@
|
|||||||
import * as program from 'commander';
|
import * as program from "commander";
|
||||||
|
|
||||||
import { EnvironmentService } from 'jslib-common/abstractions/environment.service';
|
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
|
||||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
|
|
||||||
import { ConfigurationService } from '../services/configuration.service';
|
import { StateService } from "../abstractions/state.service";
|
||||||
|
|
||||||
import { DirectoryType } from '../enums/directoryType';
|
import { DirectoryType } from "../enums/directoryType";
|
||||||
|
|
||||||
import { Response } from 'jslib-node/cli/models/response';
|
import { Response } from "jslib-node/cli/models/response";
|
||||||
import { MessageResponse } from 'jslib-node/cli/models/response/messageResponse';
|
import { MessageResponse } from "jslib-node/cli/models/response/messageResponse";
|
||||||
|
|
||||||
import { AzureConfiguration } from '../models/azureConfiguration';
|
import { AzureConfiguration } from "../models/azureConfiguration";
|
||||||
import { GSuiteConfiguration } from '../models/gsuiteConfiguration';
|
import { GSuiteConfiguration } from "../models/gsuiteConfiguration";
|
||||||
import { LdapConfiguration } from '../models/ldapConfiguration';
|
import { LdapConfiguration } from "../models/ldapConfiguration";
|
||||||
import { OktaConfiguration } from '../models/oktaConfiguration';
|
import { OktaConfiguration } from "../models/oktaConfiguration";
|
||||||
import { OneLoginConfiguration } from '../models/oneLoginConfiguration';
|
import { OneLoginConfiguration } from "../models/oneLoginConfiguration";
|
||||||
import { SyncConfiguration } from '../models/syncConfiguration';
|
import { SyncConfiguration } from "../models/syncConfiguration";
|
||||||
|
|
||||||
import { ConnectorUtils } from '../utils';
|
import { ConnectorUtils } from "../utils";
|
||||||
|
|
||||||
import { NodeUtils } from 'jslib-common/misc/nodeUtils';
|
import { NodeUtils } from "jslib-common/misc/nodeUtils";
|
||||||
|
|
||||||
export class ConfigCommand {
|
export class ConfigCommand {
|
||||||
private directory: DirectoryType;
|
private directory: DirectoryType;
|
||||||
private ldap = new LdapConfiguration();
|
private ldap = new LdapConfiguration();
|
||||||
private gsuite = new GSuiteConfiguration();
|
private gsuite = new GSuiteConfiguration();
|
||||||
private azure = new AzureConfiguration();
|
private azure = new AzureConfiguration();
|
||||||
private okta = new OktaConfiguration();
|
private okta = new OktaConfiguration();
|
||||||
private oneLogin = new OneLoginConfiguration();
|
private oneLogin = new OneLoginConfiguration();
|
||||||
private sync = new SyncConfiguration();
|
private sync = new SyncConfiguration();
|
||||||
|
|
||||||
constructor(private environmentService: EnvironmentService, private i18nService: I18nService,
|
constructor(
|
||||||
private configurationService: ConfigurationService) { }
|
private environmentService: EnvironmentService,
|
||||||
|
private i18nService: I18nService,
|
||||||
|
private stateService: StateService
|
||||||
|
) {}
|
||||||
|
|
||||||
async run(setting: string, value: string, options: program.OptionValues): Promise<Response> {
|
async run(setting: string, value: string, options: program.OptionValues): Promise<Response> {
|
||||||
setting = setting.toLowerCase();
|
setting = setting.toLowerCase();
|
||||||
if (value == null || value === '') {
|
if (value == null || value === "") {
|
||||||
if (options.secretfile) {
|
if (options.secretfile) {
|
||||||
value = await NodeUtils.readFirstLine(options.secretfile);
|
value = await NodeUtils.readFirstLine(options.secretfile);
|
||||||
} else if (options.secretenv && process.env[options.secretenv]) {
|
} else if (options.secretenv && process.env[options.secretenv]) {
|
||||||
value = process.env[options.secretenv];
|
value = process.env[options.secretenv];
|
||||||
}
|
}
|
||||||
}
|
|
||||||
try {
|
|
||||||
switch (setting) {
|
|
||||||
case 'server':
|
|
||||||
await this.setServer(value);
|
|
||||||
break;
|
|
||||||
case 'directory':
|
|
||||||
await this.setDirectory(value);
|
|
||||||
break;
|
|
||||||
case 'ldap.password':
|
|
||||||
await this.setLdapPassword(value);
|
|
||||||
break;
|
|
||||||
case 'gsuite.key':
|
|
||||||
await this.setGSuiteKey(value);
|
|
||||||
break;
|
|
||||||
case 'azure.key':
|
|
||||||
await this.setAzureKey(value);
|
|
||||||
break;
|
|
||||||
case 'okta.token':
|
|
||||||
await this.setOktaToken(value);
|
|
||||||
break;
|
|
||||||
case 'onelogin.secret':
|
|
||||||
await this.setOneLoginSecret(value);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return Response.badRequest('Unknown setting.');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
return Response.error(e);
|
|
||||||
}
|
|
||||||
const res = new MessageResponse(this.i18nService.t('savedSetting', setting), null);
|
|
||||||
return Response.success(res);
|
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
private async setServer(url: string) {
|
switch (setting) {
|
||||||
url = (url === 'null' || url === 'bitwarden.com' || url === 'https://bitwarden.com' ? null : url);
|
case "server":
|
||||||
await this.environmentService.setUrls({
|
await this.setServer(value);
|
||||||
base: url,
|
break;
|
||||||
});
|
case "directory":
|
||||||
|
await this.setDirectory(value);
|
||||||
|
break;
|
||||||
|
case "ldap.password":
|
||||||
|
await this.setLdapPassword(value);
|
||||||
|
break;
|
||||||
|
case "gsuite.key":
|
||||||
|
await this.setGSuiteKey(value);
|
||||||
|
break;
|
||||||
|
case "azure.key":
|
||||||
|
await this.setAzureKey(value);
|
||||||
|
break;
|
||||||
|
case "okta.token":
|
||||||
|
await this.setOktaToken(value);
|
||||||
|
break;
|
||||||
|
case "onelogin.secret":
|
||||||
|
await this.setOneLoginSecret(value);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return Response.badRequest("Unknown setting.");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return Response.error(e);
|
||||||
}
|
}
|
||||||
|
const res = new MessageResponse(this.i18nService.t("savedSetting", setting), null);
|
||||||
|
return Response.success(res);
|
||||||
|
}
|
||||||
|
|
||||||
private async setDirectory(type: string) {
|
private async setServer(url: string) {
|
||||||
const dir = parseInt(type, null);
|
url = url === "null" || url === "bitwarden.com" || url === "https://bitwarden.com" ? null : url;
|
||||||
if (dir < DirectoryType.Ldap || dir > DirectoryType.OneLogin) {
|
await this.environmentService.setUrls({
|
||||||
throw new Error('Invalid directory type value.');
|
base: url,
|
||||||
}
|
});
|
||||||
await this.loadConfig();
|
}
|
||||||
this.directory = dir;
|
|
||||||
await this.saveConfig();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async setLdapPassword(password: string) {
|
private async setDirectory(type: string) {
|
||||||
await this.loadConfig();
|
const dir = parseInt(type, null);
|
||||||
this.ldap.password = password;
|
if (dir < DirectoryType.Ldap || dir > DirectoryType.OneLogin) {
|
||||||
await this.saveConfig();
|
throw new Error("Invalid directory type value.");
|
||||||
}
|
}
|
||||||
|
await this.loadConfig();
|
||||||
|
this.directory = dir;
|
||||||
|
await this.saveConfig();
|
||||||
|
}
|
||||||
|
|
||||||
private async setGSuiteKey(key: string) {
|
private async setLdapPassword(password: string) {
|
||||||
await this.loadConfig();
|
await this.loadConfig();
|
||||||
this.gsuite.privateKey = key != null ? key.trimLeft() : null;
|
this.ldap.password = password;
|
||||||
await this.saveConfig();
|
await this.saveConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async setAzureKey(key: string) {
|
private async setGSuiteKey(key: string) {
|
||||||
await this.loadConfig();
|
await this.loadConfig();
|
||||||
this.azure.key = key;
|
this.gsuite.privateKey = key != null ? key.trimLeft() : null;
|
||||||
await this.saveConfig();
|
await this.saveConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async setOktaToken(token: string) {
|
private async setAzureKey(key: string) {
|
||||||
await this.loadConfig();
|
await this.loadConfig();
|
||||||
this.okta.token = token;
|
this.azure.key = key;
|
||||||
await this.saveConfig();
|
await this.saveConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async setOneLoginSecret(secret: string) {
|
private async setOktaToken(token: string) {
|
||||||
await this.loadConfig();
|
await this.loadConfig();
|
||||||
this.oneLogin.clientSecret = secret;
|
this.okta.token = token;
|
||||||
await this.saveConfig();
|
await this.saveConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadConfig() {
|
private async setOneLoginSecret(secret: string) {
|
||||||
this.directory = await this.configurationService.getDirectoryType();
|
await this.loadConfig();
|
||||||
this.ldap = (await this.configurationService.getDirectory<LdapConfiguration>(DirectoryType.Ldap)) ||
|
this.oneLogin.clientSecret = secret;
|
||||||
this.ldap;
|
await this.saveConfig();
|
||||||
this.gsuite = (await this.configurationService.getDirectory<GSuiteConfiguration>(DirectoryType.GSuite)) ||
|
}
|
||||||
this.gsuite;
|
|
||||||
this.azure = (await this.configurationService.getDirectory<AzureConfiguration>(
|
|
||||||
DirectoryType.AzureActiveDirectory)) || this.azure;
|
|
||||||
this.okta = (await this.configurationService.getDirectory<OktaConfiguration>(
|
|
||||||
DirectoryType.Okta)) || this.okta;
|
|
||||||
this.oneLogin = (await this.configurationService.getDirectory<OneLoginConfiguration>(
|
|
||||||
DirectoryType.OneLogin)) || this.oneLogin;
|
|
||||||
this.sync = (await this.configurationService.getSync()) || this.sync;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async saveConfig() {
|
private async loadConfig() {
|
||||||
ConnectorUtils.adjustConfigForSave(this.ldap, this.sync);
|
this.directory = await this.stateService.getDirectoryType();
|
||||||
await this.configurationService.saveDirectoryType(this.directory);
|
this.ldap =
|
||||||
await this.configurationService.saveDirectory(DirectoryType.Ldap, this.ldap);
|
(await this.stateService.getDirectory<LdapConfiguration>(DirectoryType.Ldap)) || this.ldap;
|
||||||
await this.configurationService.saveDirectory(DirectoryType.GSuite, this.gsuite);
|
this.gsuite =
|
||||||
await this.configurationService.saveDirectory(DirectoryType.AzureActiveDirectory, this.azure);
|
(await this.stateService.getDirectory<GSuiteConfiguration>(DirectoryType.GSuite)) ||
|
||||||
await this.configurationService.saveDirectory(DirectoryType.Okta, this.okta);
|
this.gsuite;
|
||||||
await this.configurationService.saveDirectory(DirectoryType.OneLogin, this.oneLogin);
|
this.azure =
|
||||||
await this.configurationService.saveSync(this.sync);
|
(await this.stateService.getDirectory<AzureConfiguration>(
|
||||||
}
|
DirectoryType.AzureActiveDirectory
|
||||||
|
)) || this.azure;
|
||||||
|
this.okta =
|
||||||
|
(await this.stateService.getDirectory<OktaConfiguration>(DirectoryType.Okta)) || this.okta;
|
||||||
|
this.oneLogin =
|
||||||
|
(await this.stateService.getDirectory<OneLoginConfiguration>(DirectoryType.OneLogin)) ||
|
||||||
|
this.oneLogin;
|
||||||
|
this.sync = (await this.stateService.getSync()) || this.sync;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async saveConfig() {
|
||||||
|
ConnectorUtils.adjustConfigForSave(this.ldap, this.sync);
|
||||||
|
await this.stateService.setDirectoryType(this.directory);
|
||||||
|
await this.stateService.setDirectory(DirectoryType.Ldap, this.ldap);
|
||||||
|
await this.stateService.setDirectory(DirectoryType.GSuite, this.gsuite);
|
||||||
|
await this.stateService.setDirectory(DirectoryType.AzureActiveDirectory, this.azure);
|
||||||
|
await this.stateService.setDirectory(DirectoryType.Okta, this.okta);
|
||||||
|
await this.stateService.setDirectory(DirectoryType.OneLogin, this.oneLogin);
|
||||||
|
await this.stateService.setSync(this.sync);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,31 @@
|
|||||||
import * as program from 'commander';
|
import * as program from "commander";
|
||||||
|
|
||||||
import { ConfigurationService } from '../services/configuration.service';
|
import { StateService } from "../abstractions/state.service";
|
||||||
|
|
||||||
import { Response } from 'jslib-node/cli/models/response';
|
import { Response } from "jslib-node/cli/models/response";
|
||||||
import { StringResponse } from 'jslib-node/cli/models/response/stringResponse';
|
import { StringResponse } from "jslib-node/cli/models/response/stringResponse";
|
||||||
|
|
||||||
export class LastSyncCommand {
|
export class LastSyncCommand {
|
||||||
constructor(private configurationService: ConfigurationService) { }
|
constructor(private stateService: StateService) {}
|
||||||
|
|
||||||
async run(object: string): Promise<Response> {
|
async run(object: string): Promise<Response> {
|
||||||
try {
|
try {
|
||||||
switch (object.toLowerCase()) {
|
switch (object.toLowerCase()) {
|
||||||
case 'groups':
|
case "groups":
|
||||||
const groupsDate = await this.configurationService.getLastGroupSyncDate();
|
const groupsDate = await this.stateService.getLastGroupSync();
|
||||||
const groupsRes = new StringResponse(groupsDate == null ? null : groupsDate.toISOString());
|
const groupsRes = new StringResponse(
|
||||||
return Response.success(groupsRes);
|
groupsDate == null ? null : groupsDate.toISOString()
|
||||||
case 'users':
|
);
|
||||||
const usersDate = await this.configurationService.getLastUserSyncDate();
|
return Response.success(groupsRes);
|
||||||
const usersRes = new StringResponse(usersDate == null ? null : usersDate.toISOString());
|
case "users":
|
||||||
return Response.success(usersRes);
|
const usersDate = await this.stateService.getLastUserSync();
|
||||||
default:
|
const usersRes = new StringResponse(usersDate == null ? null : usersDate.toISOString());
|
||||||
return Response.badRequest('Unknown object.');
|
return Response.success(usersRes);
|
||||||
}
|
default:
|
||||||
} catch (e) {
|
return Response.badRequest("Unknown object.");
|
||||||
return Response.error(e);
|
}
|
||||||
}
|
} catch (e) {
|
||||||
|
return Response.error(e);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,25 @@
|
|||||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
|
|
||||||
import { SyncService } from '../services/sync.service';
|
import { SyncService } from "../services/sync.service";
|
||||||
|
|
||||||
import { Response } from 'jslib-node/cli/models/response';
|
import { Response } from "jslib-node/cli/models/response";
|
||||||
import { MessageResponse } from 'jslib-node/cli/models/response/messageResponse';
|
import { MessageResponse } from "jslib-node/cli/models/response/messageResponse";
|
||||||
|
|
||||||
export class SyncCommand {
|
export class SyncCommand {
|
||||||
constructor(private syncService: SyncService, private i18nService: I18nService) { }
|
constructor(private syncService: SyncService, private i18nService: I18nService) {}
|
||||||
|
|
||||||
async run(): Promise<Response> {
|
async run(): Promise<Response> {
|
||||||
try {
|
try {
|
||||||
const result = await this.syncService.sync(false, false);
|
const result = await this.syncService.sync(false, false);
|
||||||
const groupCount = result[0] != null ? result[0].length : 0;
|
const groupCount = result[0] != null ? result[0].length : 0;
|
||||||
const userCount = result[1] != null ? result[1].length : 0;
|
const userCount = result[1] != null ? result[1].length : 0;
|
||||||
const res = new MessageResponse(this.i18nService.t('syncingComplete'),
|
const res = new MessageResponse(
|
||||||
this.i18nService.t('syncCounts', groupCount.toString(), userCount.toString()));
|
this.i18nService.t("syncingComplete"),
|
||||||
return Response.success(res);
|
this.i18nService.t("syncCounts", groupCount.toString(), userCount.toString())
|
||||||
} catch (e) {
|
);
|
||||||
return Response.error(e);
|
return Response.success(res);
|
||||||
}
|
} catch (e) {
|
||||||
|
return Response.error(e);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,28 @@
|
|||||||
import * as program from 'commander';
|
import * as program from "commander";
|
||||||
|
|
||||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
|
|
||||||
import { SyncService } from '../services/sync.service';
|
import { SyncService } from "../services/sync.service";
|
||||||
|
|
||||||
import { ConnectorUtils } from '../utils';
|
import { ConnectorUtils } from "../utils";
|
||||||
|
|
||||||
import { Response } from 'jslib-node/cli/models/response';
|
import { Response } from "jslib-node/cli/models/response";
|
||||||
import { TestResponse } from '../models/response/testResponse';
|
import { TestResponse } from "../models/response/testResponse";
|
||||||
|
|
||||||
export class TestCommand {
|
export class TestCommand {
|
||||||
constructor(private syncService: SyncService, private i18nService: I18nService) { }
|
constructor(private syncService: SyncService, private i18nService: I18nService) {}
|
||||||
|
|
||||||
async run(cmd: program.OptionValues): Promise<Response> {
|
async run(cmd: program.OptionValues): Promise<Response> {
|
||||||
try {
|
try {
|
||||||
const result = await ConnectorUtils.simulate(this.syncService, this.i18nService, cmd.last || false);
|
const result = await ConnectorUtils.simulate(
|
||||||
const res = new TestResponse(result);
|
this.syncService,
|
||||||
return Response.success(res);
|
this.i18nService,
|
||||||
} catch (e) {
|
cmd.last || false
|
||||||
return Response.error(e);
|
);
|
||||||
}
|
const res = new TestResponse(result);
|
||||||
|
return Response.success(res);
|
||||||
|
} catch (e) {
|
||||||
|
return Response.error(e);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export enum DirectoryType {
|
export enum DirectoryType {
|
||||||
Ldap = 0,
|
Ldap = 0,
|
||||||
AzureActiveDirectory = 1,
|
AzureActiveDirectory = 1,
|
||||||
GSuite = 2,
|
GSuite = 2,
|
||||||
Okta = 3,
|
Okta = 3,
|
||||||
OneLogin = 4,
|
OneLogin = 4,
|
||||||
}
|
}
|
||||||
|
|||||||
2
src/global.d.ts
vendored
2
src/global.d.ts
vendored
@@ -1,3 +1,3 @@
|
|||||||
declare function escape(s: string): string;
|
declare function escape(s: string): string;
|
||||||
declare function unescape(s: string): string;
|
declare function unescape(s: string): string;
|
||||||
declare module 'duo_web_sdk';
|
declare module "duo_web_sdk";
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline';
|
<meta
|
||||||
img-src 'self' data: *; child-src *; frame-src *; connect-src *;">
|
http-equiv="Content-Security-Policy"
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
content="default-src 'self'; style-src 'self' 'unsafe-inline';
|
||||||
|
img-src 'self' data: *; child-src *; frame-src *; connect-src *;"
|
||||||
|
/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>Bitwarden Directory Connector</title>
|
<title>Bitwarden Directory Connector</title>
|
||||||
<base href="">
|
<base href="" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<app-root>
|
<app-root>
|
||||||
<div id="loading"><i class="fa fa-spinner fa-spin fa-3x"></i></div>
|
<div id="loading"><i class="bwi bwi-spinner bwi-spin bwi-3x"></i></div>
|
||||||
</app-root>
|
</app-root>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -354,6 +354,9 @@
|
|||||||
"rootPath": {
|
"rootPath": {
|
||||||
"message": "Root Path"
|
"message": "Root Path"
|
||||||
},
|
},
|
||||||
|
"identityAuthority": {
|
||||||
|
"message": "Identity Authority"
|
||||||
|
},
|
||||||
"tenant": {
|
"tenant": {
|
||||||
"message": "Tenant"
|
"message": "Tenant"
|
||||||
},
|
},
|
||||||
|
|||||||
232
src/main.ts
232
src/main.ts
@@ -1,109 +1,155 @@
|
|||||||
import { app } from 'electron';
|
import { app } from "electron";
|
||||||
import * as path from 'path';
|
import * as path from "path";
|
||||||
|
|
||||||
import { MenuMain } from './main/menu.main';
|
import { MenuMain } from "./main/menu.main";
|
||||||
import { MessagingMain } from './main/messaging.main';
|
import { MessagingMain } from "./main/messaging.main";
|
||||||
import { I18nService } from './services/i18n.service';
|
import { I18nService } from "./services/i18n.service";
|
||||||
|
|
||||||
import { KeytarStorageListener } from 'jslib-electron/keytarStorageListener';
|
import { KeytarStorageListener } from "jslib-electron/keytarStorageListener";
|
||||||
import { ElectronLogService } from 'jslib-electron/services/electronLog.service';
|
import { ElectronLogService } from "jslib-electron/services/electronLog.service";
|
||||||
import { ElectronMainMessagingService } from 'jslib-electron/services/electronMainMessaging.service';
|
import { ElectronMainMessagingService } from "jslib-electron/services/electronMainMessaging.service";
|
||||||
import { ElectronStorageService } from 'jslib-electron/services/electronStorage.service';
|
import { ElectronStorageService } from "jslib-electron/services/electronStorage.service";
|
||||||
import { TrayMain } from 'jslib-electron/tray.main';
|
import { TrayMain } from "jslib-electron/tray.main";
|
||||||
import { UpdaterMain } from 'jslib-electron/updater.main';
|
import { UpdaterMain } from "jslib-electron/updater.main";
|
||||||
import { WindowMain } from 'jslib-electron/window.main';
|
import { WindowMain } from "jslib-electron/window.main";
|
||||||
|
|
||||||
|
import { StateService } from "./services/state.service";
|
||||||
|
|
||||||
|
import { Account } from "./models/account";
|
||||||
|
|
||||||
|
import { StateFactory } from "jslib-common/factories/stateFactory";
|
||||||
|
|
||||||
|
import { GlobalState } from "jslib-common/models/domain/globalState";
|
||||||
|
|
||||||
export class Main {
|
export class Main {
|
||||||
logService: ElectronLogService;
|
logService: ElectronLogService;
|
||||||
i18nService: I18nService;
|
i18nService: I18nService;
|
||||||
storageService: ElectronStorageService;
|
storageService: ElectronStorageService;
|
||||||
messagingService: ElectronMainMessagingService;
|
messagingService: ElectronMainMessagingService;
|
||||||
keytarStorageListener: KeytarStorageListener;
|
keytarStorageListener: KeytarStorageListener;
|
||||||
|
stateService: StateService;
|
||||||
|
|
||||||
windowMain: WindowMain;
|
windowMain: WindowMain;
|
||||||
messagingMain: MessagingMain;
|
messagingMain: MessagingMain;
|
||||||
menuMain: MenuMain;
|
menuMain: MenuMain;
|
||||||
updaterMain: UpdaterMain;
|
updaterMain: UpdaterMain;
|
||||||
trayMain: TrayMain;
|
trayMain: TrayMain;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
// Set paths for portable builds
|
// Set paths for portable builds
|
||||||
let appDataPath = null;
|
let appDataPath = null;
|
||||||
if (process.env.BITWARDEN_CONNECTOR_APPDATA_DIR != null) {
|
if (process.env.BITWARDEN_CONNECTOR_APPDATA_DIR != null) {
|
||||||
appDataPath = process.env.BITWARDEN_CONNECTOR_APPDATA_DIR;
|
appDataPath = process.env.BITWARDEN_CONNECTOR_APPDATA_DIR;
|
||||||
} else if (process.platform === 'win32' && process.env.PORTABLE_EXECUTABLE_DIR != null) {
|
} else if (process.platform === "win32" && process.env.PORTABLE_EXECUTABLE_DIR != null) {
|
||||||
appDataPath = path.join(process.env.PORTABLE_EXECUTABLE_DIR, 'bitwarden-connector-appdata');
|
appDataPath = path.join(process.env.PORTABLE_EXECUTABLE_DIR, "bitwarden-connector-appdata");
|
||||||
}
|
|
||||||
|
|
||||||
if (appDataPath != null) {
|
|
||||||
app.setPath('userData', appDataPath);
|
|
||||||
}
|
|
||||||
app.setPath('logs', path.join(app.getPath('userData'), 'logs'));
|
|
||||||
|
|
||||||
const args = process.argv.slice(1);
|
|
||||||
const watch = args.some(val => val === '--watch');
|
|
||||||
|
|
||||||
if (watch) {
|
|
||||||
// tslint:disable-next-line
|
|
||||||
require('electron-reload')(__dirname, {});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logService = new ElectronLogService(null, app.getPath('userData'));
|
|
||||||
this.i18nService = new I18nService('en', './locales/');
|
|
||||||
this.storageService = new ElectronStorageService(app.getPath('userData'));
|
|
||||||
|
|
||||||
this.windowMain = new WindowMain(this.storageService, false, 800, 600, arg => this.processDeepLink(arg), null);
|
|
||||||
this.menuMain = new MenuMain(this);
|
|
||||||
this.updaterMain = new UpdaterMain(this.i18nService, this.windowMain, 'directory-connector', () => {
|
|
||||||
this.messagingService.send('checkingForUpdate');
|
|
||||||
}, () => {
|
|
||||||
this.messagingService.send('doneCheckingForUpdate');
|
|
||||||
}, () => {
|
|
||||||
this.messagingService.send('doneCheckingForUpdate');
|
|
||||||
}, 'bitwardenDirectoryConnector');
|
|
||||||
this.trayMain = new TrayMain(this.windowMain, this.i18nService, this.storageService);
|
|
||||||
this.messagingMain = new MessagingMain(this.windowMain, this.menuMain, this.updaterMain, this.trayMain);
|
|
||||||
this.messagingService = new ElectronMainMessagingService(this.windowMain, message => {
|
|
||||||
this.messagingMain.onMessage(message);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.keytarStorageListener = new KeytarStorageListener('Bitwarden Directory Connector', null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bootstrap() {
|
if (appDataPath != null) {
|
||||||
this.keytarStorageListener.init();
|
app.setPath("userData", appDataPath);
|
||||||
this.windowMain.init().then(async () => {
|
}
|
||||||
await this.i18nService.init(app.getLocale());
|
app.setPath("logs", path.join(app.getPath("userData"), "logs"));
|
||||||
this.menuMain.init();
|
|
||||||
this.messagingMain.init();
|
|
||||||
await this.updaterMain.init();
|
|
||||||
await this.trayMain.init(this.i18nService.t('bitwardenDirectoryConnector'));
|
|
||||||
|
|
||||||
if (!app.isDefaultProtocolClient('bwdc')) {
|
const args = process.argv.slice(1);
|
||||||
app.setAsDefaultProtocolClient('bwdc');
|
const watch = args.some((val) => val === "--watch");
|
||||||
}
|
|
||||||
|
|
||||||
// Process protocol for macOS
|
if (watch) {
|
||||||
app.on('open-url', (event, url) => {
|
// tslint:disable-next-line
|
||||||
event.preventDefault();
|
require("electron-reload")(__dirname, {});
|
||||||
this.processDeepLink([url]);
|
|
||||||
});
|
|
||||||
}, (e: any) => {
|
|
||||||
// tslint:disable-next-line
|
|
||||||
console.error(e);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private processDeepLink(argv: string[]): void {
|
this.logService = new ElectronLogService(null, app.getPath("userData"));
|
||||||
argv.filter(s => s.indexOf('bwdc://') === 0).forEach(s => {
|
this.i18nService = new I18nService("en", "./locales/");
|
||||||
const url = new URL(s);
|
this.storageService = new ElectronStorageService(app.getPath("userData"));
|
||||||
const code = url.searchParams.get('code');
|
this.stateService = new StateService(
|
||||||
const receivedState = url.searchParams.get('state');
|
this.storageService,
|
||||||
if (code != null && receivedState != null) {
|
null,
|
||||||
this.messagingService.send('ssoCallback', { code: code, state: receivedState });
|
this.logService,
|
||||||
}
|
null,
|
||||||
|
true,
|
||||||
|
new StateFactory(GlobalState, Account)
|
||||||
|
);
|
||||||
|
|
||||||
|
this.windowMain = new WindowMain(
|
||||||
|
this.stateService,
|
||||||
|
this.logService,
|
||||||
|
false,
|
||||||
|
800,
|
||||||
|
600,
|
||||||
|
(arg) => this.processDeepLink(arg),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
this.menuMain = new MenuMain(this);
|
||||||
|
this.updaterMain = new UpdaterMain(
|
||||||
|
this.i18nService,
|
||||||
|
this.windowMain,
|
||||||
|
"directory-connector",
|
||||||
|
() => {
|
||||||
|
this.messagingService.send("checkingForUpdate");
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
this.messagingService.send("doneCheckingForUpdate");
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
this.messagingService.send("doneCheckingForUpdate");
|
||||||
|
},
|
||||||
|
"bitwardenDirectoryConnector"
|
||||||
|
);
|
||||||
|
|
||||||
|
this.trayMain = new TrayMain(this.windowMain, this.i18nService, this.stateService);
|
||||||
|
|
||||||
|
this.messagingMain = new MessagingMain(
|
||||||
|
this.windowMain,
|
||||||
|
this.menuMain,
|
||||||
|
this.updaterMain,
|
||||||
|
this.trayMain
|
||||||
|
);
|
||||||
|
this.messagingService = new ElectronMainMessagingService(this.windowMain, (message) => {
|
||||||
|
this.messagingMain.onMessage(message);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.keytarStorageListener = new KeytarStorageListener("Bitwarden Directory Connector", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
bootstrap() {
|
||||||
|
this.keytarStorageListener.init();
|
||||||
|
this.windowMain.init().then(
|
||||||
|
async () => {
|
||||||
|
await this.i18nService.init(app.getLocale());
|
||||||
|
this.menuMain.init();
|
||||||
|
this.messagingMain.init();
|
||||||
|
await this.updaterMain.init();
|
||||||
|
await this.trayMain.init(this.i18nService.t("bitwardenDirectoryConnector"));
|
||||||
|
|
||||||
|
if (!app.isDefaultProtocolClient("bwdc")) {
|
||||||
|
app.setAsDefaultProtocolClient("bwdc");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process protocol for macOS
|
||||||
|
app.on("open-url", (event, url) => {
|
||||||
|
event.preventDefault();
|
||||||
|
this.processDeepLink([url]);
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
(e: any) => {
|
||||||
|
// tslint:disable-next-line
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private processDeepLink(argv: string[]): void {
|
||||||
|
argv
|
||||||
|
.filter((s) => s.indexOf("bwdc://") === 0)
|
||||||
|
.forEach((s) => {
|
||||||
|
const url = new URL(s);
|
||||||
|
const code = url.searchParams.get("code");
|
||||||
|
const receivedState = url.searchParams.get("state");
|
||||||
|
if (code != null && receivedState != null) {
|
||||||
|
this.messagingService.send("ssoCallback", { code: code, state: receivedState });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const main = new Main();
|
const main = new Main();
|
||||||
|
|||||||
@@ -1,68 +1,69 @@
|
|||||||
import {
|
import { Menu, MenuItem, MenuItemConstructorOptions } from "electron";
|
||||||
Menu,
|
|
||||||
MenuItem,
|
|
||||||
MenuItemConstructorOptions,
|
|
||||||
} from 'electron';
|
|
||||||
|
|
||||||
import { Main } from '../main';
|
import { Main } from "../main";
|
||||||
|
|
||||||
import { BaseMenu } from 'jslib-electron/baseMenu';
|
import { BaseMenu } from "jslib-electron/baseMenu";
|
||||||
|
|
||||||
export class MenuMain extends BaseMenu {
|
export class MenuMain extends BaseMenu {
|
||||||
menu: Menu;
|
menu: Menu;
|
||||||
|
|
||||||
constructor(private main: Main) {
|
constructor(private main: Main) {
|
||||||
super(main.i18nService, main.windowMain);
|
super(main.i18nService, main.windowMain);
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.initProperties();
|
||||||
|
this.initContextMenu();
|
||||||
|
this.initApplicationMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
private initApplicationMenu() {
|
||||||
|
const template: MenuItemConstructorOptions[] = [
|
||||||
|
this.editMenuItemOptions,
|
||||||
|
{
|
||||||
|
label: this.i18nService.t("view"),
|
||||||
|
submenu: this.viewSubMenuItemOptions,
|
||||||
|
},
|
||||||
|
this.windowMenuItemOptions,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (process.platform === "darwin") {
|
||||||
|
const firstMenuPart: MenuItemConstructorOptions[] = [
|
||||||
|
{
|
||||||
|
label: this.i18nService.t("aboutBitwarden"),
|
||||||
|
role: "about",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
template.unshift({
|
||||||
|
label: this.main.i18nService.t("bitwardenDirectoryConnector"),
|
||||||
|
submenu: firstMenuPart.concat(this.macAppMenuItemOptions),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Window menu
|
||||||
|
template[template.length - 1].submenu = this.macWindowSubmenuOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
(template[template.length - 1].submenu as MenuItemConstructorOptions[]).splice(
|
||||||
this.initProperties();
|
1,
|
||||||
this.initContextMenu();
|
0,
|
||||||
this.initApplicationMenu();
|
{
|
||||||
}
|
label: this.main.i18nService.t(
|
||||||
|
process.platform === "darwin" ? "hideToMenuBar" : "hideToTray"
|
||||||
|
),
|
||||||
|
click: () => this.main.messagingService.send("hideToTray"),
|
||||||
|
accelerator: "CmdOrCtrl+Shift+M",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "checkbox",
|
||||||
|
label: this.main.i18nService.t("alwaysOnTop"),
|
||||||
|
checked: this.windowMain.win.isAlwaysOnTop(),
|
||||||
|
click: () => this.main.windowMain.toggleAlwaysOnTop(),
|
||||||
|
accelerator: "CmdOrCtrl+Shift+T",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
private initApplicationMenu() {
|
this.menu = Menu.buildFromTemplate(template);
|
||||||
const template: MenuItemConstructorOptions[] = [
|
Menu.setApplicationMenu(this.menu);
|
||||||
this.editMenuItemOptions,
|
}
|
||||||
{
|
|
||||||
label: this.i18nService.t('view'),
|
|
||||||
submenu: this.viewSubMenuItemOptions,
|
|
||||||
},
|
|
||||||
this.windowMenuItemOptions,
|
|
||||||
];
|
|
||||||
|
|
||||||
if (process.platform === 'darwin') {
|
|
||||||
const firstMenuPart: MenuItemConstructorOptions[] = [
|
|
||||||
{
|
|
||||||
label: this.i18nService.t('aboutBitwarden'),
|
|
||||||
role: 'about',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
template.unshift({
|
|
||||||
label: this.main.i18nService.t('bitwardenDirectoryConnector'),
|
|
||||||
submenu: firstMenuPart.concat(this.macAppMenuItemOptions),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Window menu
|
|
||||||
template[template.length - 1].submenu = this.macWindowSubmenuOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
(template[template.length - 1].submenu as MenuItemConstructorOptions[]).splice(1, 0,
|
|
||||||
{
|
|
||||||
label: this.main.i18nService.t(process.platform === 'darwin' ? 'hideToMenuBar' : 'hideToTray'),
|
|
||||||
click: () => this.main.messagingService.send('hideToTray'),
|
|
||||||
accelerator: 'CmdOrCtrl+Shift+M',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'checkbox',
|
|
||||||
label: this.main.i18nService.t('alwaysOnTop'),
|
|
||||||
checked: this.windowMain.win.isAlwaysOnTop(),
|
|
||||||
click: () => this.main.windowMain.toggleAlwaysOnTop(),
|
|
||||||
accelerator: 'CmdOrCtrl+Shift+T',
|
|
||||||
});
|
|
||||||
|
|
||||||
this.menu = Menu.buildFromTemplate(template);
|
|
||||||
Menu.setApplicationMenu(this.menu);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,67 +1,68 @@
|
|||||||
import {
|
import { app, ipcMain } from "electron";
|
||||||
app,
|
|
||||||
ipcMain,
|
|
||||||
} from 'electron';
|
|
||||||
|
|
||||||
import { TrayMain } from 'jslib-electron/tray.main';
|
import { TrayMain } from "jslib-electron/tray.main";
|
||||||
import { UpdaterMain } from 'jslib-electron/updater.main';
|
import { UpdaterMain } from "jslib-electron/updater.main";
|
||||||
import { WindowMain } from 'jslib-electron/window.main';
|
import { WindowMain } from "jslib-electron/window.main";
|
||||||
|
|
||||||
import { MenuMain } from './menu.main';
|
import { MenuMain } from "./menu.main";
|
||||||
|
|
||||||
const SyncCheckInterval = 60 * 1000; // 1 minute
|
const SyncCheckInterval = 60 * 1000; // 1 minute
|
||||||
|
|
||||||
export class MessagingMain {
|
export class MessagingMain {
|
||||||
private syncTimeout: NodeJS.Timer;
|
private syncTimeout: NodeJS.Timer;
|
||||||
|
|
||||||
constructor(private windowMain: WindowMain, private menuMain: MenuMain,
|
constructor(
|
||||||
private updaterMain: UpdaterMain, private trayMain: TrayMain) { }
|
private windowMain: WindowMain,
|
||||||
|
private menuMain: MenuMain,
|
||||||
|
private updaterMain: UpdaterMain,
|
||||||
|
private trayMain: TrayMain
|
||||||
|
) {}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
ipcMain.on('messagingService', async (event: any, message: any) => this.onMessage(message));
|
ipcMain.on("messagingService", async (event: any, message: any) => this.onMessage(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
onMessage(message: any) {
|
onMessage(message: any) {
|
||||||
switch (message.command) {
|
switch (message.command) {
|
||||||
case 'checkForUpdate':
|
case "checkForUpdate":
|
||||||
this.updaterMain.checkForUpdate(true);
|
this.updaterMain.checkForUpdate(true);
|
||||||
break;
|
break;
|
||||||
case 'scheduleNextDirSync':
|
case "scheduleNextDirSync":
|
||||||
this.scheduleNextSync();
|
this.scheduleNextSync();
|
||||||
break;
|
break;
|
||||||
case 'cancelDirSync':
|
case "cancelDirSync":
|
||||||
this.windowMain.win.webContents.send('messagingService', {
|
this.windowMain.win.webContents.send("messagingService", {
|
||||||
command: 'syncScheduleStopped',
|
command: "syncScheduleStopped",
|
||||||
});
|
|
||||||
if (this.syncTimeout) {
|
|
||||||
global.clearTimeout(this.syncTimeout);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'hideToTray':
|
|
||||||
this.trayMain.hideToTray();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private scheduleNextSync() {
|
|
||||||
this.windowMain.win.webContents.send('messagingService', {
|
|
||||||
command: 'syncScheduleStarted',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.syncTimeout) {
|
if (this.syncTimeout) {
|
||||||
global.clearTimeout(this.syncTimeout);
|
global.clearTimeout(this.syncTimeout);
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
this.syncTimeout = global.setTimeout(() => {
|
case "hideToTray":
|
||||||
if (this.windowMain.win == null) {
|
this.trayMain.hideToTray();
|
||||||
return;
|
break;
|
||||||
}
|
default:
|
||||||
|
break;
|
||||||
this.windowMain.win.webContents.send('messagingService', {
|
|
||||||
command: 'checkDirSync',
|
|
||||||
});
|
|
||||||
}, SyncCheckInterval);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private scheduleNextSync() {
|
||||||
|
this.windowMain.win.webContents.send("messagingService", {
|
||||||
|
command: "syncScheduleStarted",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.syncTimeout) {
|
||||||
|
global.clearTimeout(this.syncTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.syncTimeout = global.setTimeout(() => {
|
||||||
|
if (this.windowMain.win == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.windowMain.win.webContents.send("messagingService", {
|
||||||
|
command: "checkDirSync",
|
||||||
|
});
|
||||||
|
}, SyncCheckInterval);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
2
src/models/IConfiguration.ts
Normal file
2
src/models/IConfiguration.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
// tslint:disable-next-line
|
||||||
|
export interface IConfiguration {}
|
||||||
47
src/models/account.ts
Normal file
47
src/models/account.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { Account as BaseAccount } from "jslib-common/models/domain/account";
|
||||||
|
|
||||||
|
import { DirectoryType } from "src/enums/directoryType";
|
||||||
|
|
||||||
|
import { AzureConfiguration } from "./azureConfiguration";
|
||||||
|
import { GSuiteConfiguration } from "./gsuiteConfiguration";
|
||||||
|
import { LdapConfiguration } from "./ldapConfiguration";
|
||||||
|
import { OktaConfiguration } from "./oktaConfiguration";
|
||||||
|
import { OneLoginConfiguration } from "./oneLoginConfiguration";
|
||||||
|
import { SyncConfiguration } from "./syncConfiguration";
|
||||||
|
|
||||||
|
export class Account extends BaseAccount {
|
||||||
|
directoryConfigurations?: DirectoryConfigurations = new DirectoryConfigurations();
|
||||||
|
directorySettings: DirectorySettings = new DirectorySettings();
|
||||||
|
clientKeys: ClientKeys = new ClientKeys();
|
||||||
|
|
||||||
|
constructor(init: Partial<Account>) {
|
||||||
|
super(init);
|
||||||
|
this.directoryConfigurations = init?.directoryConfigurations ?? new DirectoryConfigurations();
|
||||||
|
this.directorySettings = init?.directorySettings ?? new DirectorySettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ClientKeys {
|
||||||
|
clientId: string;
|
||||||
|
clientSecret: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DirectoryConfigurations {
|
||||||
|
ldap: LdapConfiguration;
|
||||||
|
gsuite: GSuiteConfiguration;
|
||||||
|
azure: AzureConfiguration;
|
||||||
|
okta: OktaConfiguration;
|
||||||
|
oneLogin: OneLoginConfiguration;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DirectorySettings {
|
||||||
|
organizationId?: string;
|
||||||
|
sync?: SyncConfiguration;
|
||||||
|
directoryType?: DirectoryType;
|
||||||
|
userDelta?: string;
|
||||||
|
groupDelta?: string;
|
||||||
|
lastUserSync?: Date;
|
||||||
|
lastGroupSync?: Date;
|
||||||
|
lastSyncHash?: string;
|
||||||
|
syncingDir?: boolean;
|
||||||
|
}
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
export class AzureConfiguration {
|
import { IConfiguration } from "./IConfiguration";
|
||||||
tenant: string;
|
|
||||||
applicationId: string;
|
export class AzureConfiguration implements IConfiguration {
|
||||||
key: string;
|
identityAuthority: string;
|
||||||
|
tenant: string;
|
||||||
|
applicationId: string;
|
||||||
|
key: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
export abstract class Entry {
|
export abstract class Entry {
|
||||||
referenceId: string;
|
referenceId: string;
|
||||||
externalId: string;
|
externalId: string;
|
||||||
|
|
||||||
get displayName(): string {
|
get displayName(): string {
|
||||||
return this.referenceId;
|
return this.referenceId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { Entry } from './entry';
|
import { Entry } from "./entry";
|
||||||
|
|
||||||
export class GroupEntry extends Entry {
|
export class GroupEntry extends Entry {
|
||||||
name: string;
|
name: string;
|
||||||
userMemberExternalIds = new Set<string>();
|
userMemberExternalIds = new Set<string>();
|
||||||
groupMemberReferenceIds = new Set<string>();
|
groupMemberReferenceIds = new Set<string>();
|
||||||
|
|
||||||
get displayName(): string {
|
get displayName(): string {
|
||||||
if (this.name == null) {
|
if (this.name == null) {
|
||||||
return this.referenceId;
|
return this.referenceId;
|
||||||
}
|
|
||||||
|
|
||||||
return this.name;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
export class GSuiteConfiguration {
|
import { IConfiguration } from "./IConfiguration";
|
||||||
clientEmail: string;
|
|
||||||
privateKey: string;
|
export class GSuiteConfiguration implements IConfiguration {
|
||||||
domain: string;
|
clientEmail: string;
|
||||||
adminUser: string;
|
privateKey: string;
|
||||||
customer: string;
|
domain: string;
|
||||||
|
adminUser: string;
|
||||||
|
customer: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
export class LdapConfiguration {
|
import { IConfiguration } from "./IConfiguration";
|
||||||
ssl = false;
|
|
||||||
startTls = false;
|
export class LdapConfiguration implements IConfiguration {
|
||||||
tlsCaPath: string;
|
ssl = false;
|
||||||
sslAllowUnauthorized = false;
|
startTls = false;
|
||||||
sslCertPath: string;
|
tlsCaPath: string;
|
||||||
sslKeyPath: string;
|
sslAllowUnauthorized = false;
|
||||||
sslCaPath: string;
|
sslCertPath: string;
|
||||||
hostname: string;
|
sslKeyPath: string;
|
||||||
port = 389;
|
sslCaPath: string;
|
||||||
domain: string;
|
hostname: string;
|
||||||
rootPath: string;
|
port = 389;
|
||||||
currentUser = false;
|
domain: string;
|
||||||
username: string;
|
rootPath: string;
|
||||||
password: string;
|
currentUser = false;
|
||||||
ad = true;
|
username: string;
|
||||||
pagedSearch = true;
|
password: string;
|
||||||
|
ad = true;
|
||||||
|
pagedSearch = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
export class OktaConfiguration {
|
import { IConfiguration } from "./IConfiguration";
|
||||||
orgUrl: string;
|
|
||||||
token: string;
|
export class OktaConfiguration implements IConfiguration {
|
||||||
|
orgUrl: string;
|
||||||
|
token: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
export class OneLoginConfiguration {
|
import { IConfiguration } from "./IConfiguration";
|
||||||
clientId: string;
|
|
||||||
clientSecret: string;
|
export class OneLoginConfiguration implements IConfiguration {
|
||||||
region = 'us';
|
clientId: string;
|
||||||
|
clientSecret: string;
|
||||||
|
region = "us";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { GroupEntry } from '../groupEntry';
|
import { GroupEntry } from "../groupEntry";
|
||||||
|
|
||||||
export class GroupResponse {
|
export class GroupResponse {
|
||||||
externalId: string;
|
externalId: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
userIds: string[];
|
userIds: string[];
|
||||||
|
|
||||||
constructor(g: GroupEntry) {
|
constructor(g: GroupEntry) {
|
||||||
this.externalId = g.externalId;
|
this.externalId = g.externalId;
|
||||||
this.displayName = g.displayName;
|
this.displayName = g.displayName;
|
||||||
this.userIds = Array.from(g.userMemberExternalIds);
|
this.userIds = Array.from(g.userMemberExternalIds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,25 @@
|
|||||||
import { GroupResponse } from './groupResponse';
|
import { GroupResponse } from "./groupResponse";
|
||||||
import { UserResponse } from './userResponse';
|
import { UserResponse } from "./userResponse";
|
||||||
|
|
||||||
import { SimResult } from '../simResult';
|
import { SimResult } from "../simResult";
|
||||||
|
|
||||||
import { BaseResponse } from 'jslib-node/cli/models/response/baseResponse';
|
import { BaseResponse } from "jslib-node/cli/models/response/baseResponse";
|
||||||
|
|
||||||
export class TestResponse implements BaseResponse {
|
export class TestResponse implements BaseResponse {
|
||||||
object: string;
|
object: string;
|
||||||
groups: GroupResponse[] = [];
|
groups: GroupResponse[] = [];
|
||||||
enabledUsers: UserResponse[] = [];
|
enabledUsers: UserResponse[] = [];
|
||||||
disabledUsers: UserResponse[] = [];
|
disabledUsers: UserResponse[] = [];
|
||||||
deletedUsers: UserResponse[] = [];
|
deletedUsers: UserResponse[] = [];
|
||||||
|
|
||||||
constructor(result: SimResult) {
|
constructor(result: SimResult) {
|
||||||
this.object = 'test';
|
this.object = "test";
|
||||||
this.groups = result.groups != null ? result.groups.map(g => new GroupResponse(g)) : [];
|
this.groups = result.groups != null ? result.groups.map((g) => new GroupResponse(g)) : [];
|
||||||
this.enabledUsers = result.enabledUsers != null ? result.enabledUsers.map(u => new UserResponse(u)) : [];
|
this.enabledUsers =
|
||||||
this.disabledUsers = result.disabledUsers != null ? result.disabledUsers.map(u => new UserResponse(u)) : [];
|
result.enabledUsers != null ? result.enabledUsers.map((u) => new UserResponse(u)) : [];
|
||||||
this.deletedUsers = result.deletedUsers != null ? result.deletedUsers.map(u => new UserResponse(u)) : [];
|
this.disabledUsers =
|
||||||
}
|
result.disabledUsers != null ? result.disabledUsers.map((u) => new UserResponse(u)) : [];
|
||||||
|
this.deletedUsers =
|
||||||
|
result.deletedUsers != null ? result.deletedUsers.map((u) => new UserResponse(u)) : [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { UserEntry } from '../userEntry';
|
import { UserEntry } from "../userEntry";
|
||||||
|
|
||||||
export class UserResponse {
|
export class UserResponse {
|
||||||
externalId: string;
|
externalId: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
|
|
||||||
constructor(u: UserEntry) {
|
constructor(u: UserEntry) {
|
||||||
this.externalId = u.externalId;
|
this.externalId = u.externalId;
|
||||||
this.displayName = u.displayName;
|
this.displayName = u.displayName;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { GroupEntry } from './groupEntry';
|
import { GroupEntry } from "./groupEntry";
|
||||||
import { UserEntry } from './userEntry';
|
import { UserEntry } from "./userEntry";
|
||||||
|
|
||||||
export class SimResult {
|
export class SimResult {
|
||||||
groups: GroupEntry[] = [];
|
groups: GroupEntry[] = [];
|
||||||
users: UserEntry[] = [];
|
users: UserEntry[] = [];
|
||||||
enabledUsers: UserEntry[] = [];
|
enabledUsers: UserEntry[] = [];
|
||||||
disabledUsers: UserEntry[] = [];
|
disabledUsers: UserEntry[] = [];
|
||||||
deletedUsers: UserEntry[] = [];
|
deletedUsers: UserEntry[] = [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
export class SyncConfiguration {
|
export class SyncConfiguration {
|
||||||
users = false;
|
users = false;
|
||||||
groups = false;
|
groups = false;
|
||||||
interval = 5;
|
interval = 5;
|
||||||
userFilter: string;
|
userFilter: string;
|
||||||
groupFilter: string;
|
groupFilter: string;
|
||||||
removeDisabled = false;
|
removeDisabled = false;
|
||||||
overwriteExisting = false;
|
overwriteExisting = false;
|
||||||
largeImport = false;
|
largeImport = false;
|
||||||
// Ldap properties
|
// Ldap properties
|
||||||
groupObjectClass: string;
|
groupObjectClass: string;
|
||||||
userObjectClass: string;
|
userObjectClass: string;
|
||||||
groupPath: string;
|
groupPath: string;
|
||||||
userPath: string;
|
userPath: string;
|
||||||
groupNameAttribute: string;
|
groupNameAttribute: string;
|
||||||
userEmailAttribute: string;
|
userEmailAttribute: string;
|
||||||
memberAttribute: string;
|
memberAttribute: string;
|
||||||
useEmailPrefixSuffix = false;
|
useEmailPrefixSuffix = false;
|
||||||
emailPrefixAttribute: string;
|
emailPrefixAttribute: string;
|
||||||
emailSuffix: string;
|
emailSuffix: string;
|
||||||
creationDateAttribute: string;
|
creationDateAttribute: string;
|
||||||
revisionDateAttribute: string;
|
revisionDateAttribute: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { Entry } from './entry';
|
import { Entry } from "./entry";
|
||||||
|
|
||||||
export class UserEntry extends Entry {
|
export class UserEntry extends Entry {
|
||||||
email: string;
|
email: string;
|
||||||
disabled = false;
|
disabled = false;
|
||||||
deleted = false;
|
deleted = false;
|
||||||
|
|
||||||
get displayName(): string {
|
get displayName(): string {
|
||||||
if (this.email == null) {
|
if (this.email == null) {
|
||||||
return this.referenceId;
|
return this.referenceId;
|
||||||
}
|
|
||||||
|
|
||||||
return this.email;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return this.email;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1784
src/package-lock.json
generated
Normal file
1784
src/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "bitwarden-directory-connector",
|
"name": "@bitwarden/directory-connector",
|
||||||
"productName": "Bitwarden Directory Connector",
|
"productName": "Bitwarden Directory Connector",
|
||||||
"description": "Sync your user directory to your Bitwarden organization.",
|
"description": "Sync your user directory to your Bitwarden organization.",
|
||||||
"version": "2.9.9-alpha",
|
"version": "2.9.10",
|
||||||
"author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)",
|
"author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)",
|
||||||
"homepage": "https://bitwarden.com",
|
"homepage": "https://bitwarden.com",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
@@ -13,9 +13,10 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"browser-hrtime": "^1.1.8",
|
"browser-hrtime": "^1.1.8",
|
||||||
"electron-log": "4.3.5",
|
"electron-log": "4.4.1",
|
||||||
"electron-store": "8.0.0",
|
"electron-store": "8.0.1",
|
||||||
"electron-updater": "4.3.9",
|
"electron-updater": "4.6.1",
|
||||||
"keytar": "7.6.0"
|
"keytar": "7.7.0",
|
||||||
|
"rxjs": "^7.4.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
568
src/program.ts
568
src/program.ts
@@ -1,301 +1,321 @@
|
|||||||
import * as chalk from 'chalk';
|
import * as chalk from "chalk";
|
||||||
import * as program from 'commander';
|
import * as program from "commander";
|
||||||
import * as path from 'path';
|
import * as path from "path";
|
||||||
|
|
||||||
import { Main } from './bwdc';
|
import { Main } from "./bwdc";
|
||||||
|
|
||||||
import { ClearCacheCommand } from './commands/clearCache.command';
|
import { ClearCacheCommand } from "./commands/clearCache.command";
|
||||||
import { ConfigCommand } from './commands/config.command';
|
import { ConfigCommand } from "./commands/config.command";
|
||||||
import { LastSyncCommand } from './commands/lastSync.command';
|
import { LastSyncCommand } from "./commands/lastSync.command";
|
||||||
import { SyncCommand } from './commands/sync.command';
|
import { SyncCommand } from "./commands/sync.command";
|
||||||
import { TestCommand } from './commands/test.command';
|
import { TestCommand } from "./commands/test.command";
|
||||||
|
|
||||||
import { LoginCommand } from 'jslib-node/cli/commands/login.command';
|
import { LoginCommand } from "jslib-node/cli/commands/login.command";
|
||||||
import { LogoutCommand } from 'jslib-node/cli/commands/logout.command';
|
import { LogoutCommand } from "jslib-node/cli/commands/logout.command";
|
||||||
import { UpdateCommand } from 'jslib-node/cli/commands/update.command';
|
import { UpdateCommand } from "jslib-node/cli/commands/update.command";
|
||||||
|
|
||||||
import { BaseProgram } from 'jslib-node/cli/baseProgram';
|
import { BaseProgram } from "jslib-node/cli/baseProgram";
|
||||||
|
|
||||||
import { ApiKeyService } from 'jslib-common/abstractions/apiKey.service';
|
import { Response } from "jslib-node/cli/models/response";
|
||||||
import { Response } from 'jslib-node/cli/models/response';
|
import { StringResponse } from "jslib-node/cli/models/response/stringResponse";
|
||||||
import { StringResponse } from 'jslib-node/cli/models/response/stringResponse';
|
|
||||||
|
|
||||||
import { Utils } from 'jslib-common/misc/utils';
|
import { Utils } from "jslib-common/misc/utils";
|
||||||
|
|
||||||
const writeLn = (s: string, finalLine: boolean = false, error: boolean = false) => {
|
const writeLn = (s: string, finalLine: boolean = false, error: boolean = false) => {
|
||||||
const stream = error ? process.stderr : process.stdout;
|
const stream = error ? process.stderr : process.stdout;
|
||||||
if (finalLine && process.platform === 'win32') {
|
if (finalLine && process.platform === "win32") {
|
||||||
stream.write(s);
|
stream.write(s);
|
||||||
} else {
|
} else {
|
||||||
stream.write(s + '\n');
|
stream.write(s + "\n");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export class Program extends BaseProgram {
|
export class Program extends BaseProgram {
|
||||||
private apiKeyService: ApiKeyService;
|
constructor(private main: Main) {
|
||||||
|
super(main.stateService, writeLn);
|
||||||
|
}
|
||||||
|
|
||||||
constructor(private main: Main) {
|
async run() {
|
||||||
super(main.userService, writeLn);
|
program
|
||||||
this.apiKeyService = main.apiKeyService;
|
.option("--pretty", "Format output. JSON is tabbed with two spaces.")
|
||||||
}
|
.option("--raw", "Return raw output instead of a descriptive message.")
|
||||||
|
.option("--response", "Return a JSON formatted version of response output.")
|
||||||
|
.option("--cleanexit", "Exit with a success exit code (0) unless an error is thrown.")
|
||||||
|
.option("--quiet", "Don't return anything to stdout.")
|
||||||
|
.option("--nointeraction", "Do not prompt for interactive user input.")
|
||||||
|
.version(await this.main.platformUtilsService.getApplicationVersion(), "-v, --version");
|
||||||
|
|
||||||
async run() {
|
program.on("option:pretty", () => {
|
||||||
program
|
process.env.BW_PRETTY = "true";
|
||||||
.option('--pretty', 'Format output. JSON is tabbed with two spaces.')
|
});
|
||||||
.option('--raw', 'Return raw output instead of a descriptive message.')
|
|
||||||
.option('--response', 'Return a JSON formatted version of response output.')
|
|
||||||
.option('--cleanexit', 'Exit with a success exit code (0) unless an error is thrown.')
|
|
||||||
.option('--quiet', 'Don\'t return anything to stdout.')
|
|
||||||
.option('--nointeraction', 'Do not prompt for interactive user input.')
|
|
||||||
.version(await this.main.platformUtilsService.getApplicationVersion(), '-v, --version');
|
|
||||||
|
|
||||||
program.on('option:pretty', () => {
|
program.on("option:raw", () => {
|
||||||
process.env.BW_PRETTY = 'true';
|
process.env.BW_RAW = "true";
|
||||||
});
|
});
|
||||||
|
|
||||||
program.on('option:raw', () => {
|
program.on("option:quiet", () => {
|
||||||
process.env.BW_RAW = 'true';
|
process.env.BW_QUIET = "true";
|
||||||
});
|
});
|
||||||
|
|
||||||
program.on('option:quiet', () => {
|
program.on("option:response", () => {
|
||||||
process.env.BW_QUIET = 'true';
|
process.env.BW_RESPONSE = "true";
|
||||||
});
|
});
|
||||||
|
|
||||||
program.on('option:response', () => {
|
program.on("option:cleanexit", () => {
|
||||||
process.env.BW_RESPONSE = 'true';
|
process.env.BW_CLEANEXIT = "true";
|
||||||
});
|
});
|
||||||
|
|
||||||
program.on('option:cleanexit', () => {
|
program.on("option:nointeraction", () => {
|
||||||
process.env.BW_CLEANEXIT = 'true';
|
process.env.BW_NOINTERACTION = "true";
|
||||||
});
|
});
|
||||||
|
|
||||||
program.on('option:nointeraction', () => {
|
program.on("command:*", () => {
|
||||||
process.env.BW_NOINTERACTION = 'true';
|
writeLn(chalk.redBright("Invalid command: " + program.args.join(" ")), false, true);
|
||||||
});
|
writeLn("See --help for a list of available commands.", true, true);
|
||||||
|
process.exitCode = 1;
|
||||||
|
});
|
||||||
|
|
||||||
program.on('command:*', () => {
|
program.on("--help", () => {
|
||||||
writeLn(chalk.redBright('Invalid command: ' + program.args.join(' ')), false, true);
|
writeLn("\n Examples:");
|
||||||
writeLn('See --help for a list of available commands.', true, true);
|
writeLn("");
|
||||||
process.exitCode = 1;
|
writeLn(" bwdc login");
|
||||||
});
|
writeLn(" bwdc test");
|
||||||
|
writeLn(" bwdc sync");
|
||||||
|
writeLn(" bwdc last-sync");
|
||||||
|
writeLn(" bwdc config server https://bw.company.com");
|
||||||
|
writeLn(" bwdc update");
|
||||||
|
writeLn("", true);
|
||||||
|
});
|
||||||
|
|
||||||
program.on('--help', () => {
|
program
|
||||||
writeLn('\n Examples:');
|
.command("login [clientId] [clientSecret]")
|
||||||
writeLn('');
|
.description("Log into an organization account.", {
|
||||||
writeLn(' bwdc login');
|
clientId: "Client_id part of your organization's API key",
|
||||||
writeLn(' bwdc test');
|
clientSecret: "Client_secret part of your organization's API key",
|
||||||
writeLn(' bwdc sync');
|
})
|
||||||
writeLn(' bwdc last-sync');
|
.action(async (clientId: string, clientSecret: string, options: program.OptionValues) => {
|
||||||
writeLn(' bwdc config server https://bw.company.com');
|
await this.exitIfAuthed();
|
||||||
writeLn(' bwdc update');
|
const command = new LoginCommand(
|
||||||
writeLn('', true);
|
this.main.authService,
|
||||||
});
|
this.main.apiService,
|
||||||
|
this.main.i18nService,
|
||||||
|
this.main.environmentService,
|
||||||
|
this.main.passwordGenerationService,
|
||||||
|
this.main.cryptoFunctionService,
|
||||||
|
this.main.platformUtilsService,
|
||||||
|
this.main.stateService,
|
||||||
|
this.main.cryptoService,
|
||||||
|
this.main.policyService,
|
||||||
|
"connector"
|
||||||
|
);
|
||||||
|
|
||||||
program
|
if (!Utils.isNullOrWhitespace(clientId)) {
|
||||||
.command('login [clientId] [clientSecret]')
|
process.env.BW_CLIENTID = clientId;
|
||||||
.description('Log into an organization account.', {
|
|
||||||
clientId: 'Client_id part of your organization\'s API key',
|
|
||||||
clientSecret: 'Client_secret part of your organization\'s API key',
|
|
||||||
})
|
|
||||||
.action(async (clientId: string, clientSecret: string, options: program.OptionValues) => {
|
|
||||||
await this.exitIfAuthed();
|
|
||||||
const command = new LoginCommand(this.main.authService, this.main.apiService, this.main.i18nService,
|
|
||||||
this.main.environmentService, this.main.passwordGenerationService, this.main.cryptoFunctionService,
|
|
||||||
this.main.platformUtilsService, 'connector');
|
|
||||||
|
|
||||||
if (!Utils.isNullOrWhitespace(clientId)) {
|
|
||||||
process.env.BW_CLIENTID = clientId;
|
|
||||||
}
|
|
||||||
if (!Utils.isNullOrWhitespace(clientSecret)) {
|
|
||||||
process.env.BW_CLIENTSECRET = clientSecret;
|
|
||||||
}
|
|
||||||
|
|
||||||
options = Object.assign(options ?? {}, { apikey: true }); // force apikey use
|
|
||||||
const response = await command.run(null, null, options);
|
|
||||||
this.processResponse(response);
|
|
||||||
});
|
|
||||||
|
|
||||||
program
|
|
||||||
.command('logout')
|
|
||||||
.description('Log out of the current user account.')
|
|
||||||
.on('--help', () => {
|
|
||||||
writeLn('\n Examples:');
|
|
||||||
writeLn('');
|
|
||||||
writeLn(' bwdc logout');
|
|
||||||
writeLn('', true);
|
|
||||||
})
|
|
||||||
.action(async () => {
|
|
||||||
await this.exitIfNotAuthed();
|
|
||||||
const command = new LogoutCommand(this.main.authService, this.main.i18nService,
|
|
||||||
async () => await this.main.logout());
|
|
||||||
const response = await command.run();
|
|
||||||
this.processResponse(response);
|
|
||||||
});
|
|
||||||
|
|
||||||
program
|
|
||||||
.command('test')
|
|
||||||
.description('Test a simulated sync.')
|
|
||||||
.option('-l, --last', 'Since the last successful sync.')
|
|
||||||
.on('--help', () => {
|
|
||||||
writeLn('\n Examples:');
|
|
||||||
writeLn('');
|
|
||||||
writeLn(' bwdc test');
|
|
||||||
writeLn(' bwdc test --last');
|
|
||||||
writeLn('', true);
|
|
||||||
})
|
|
||||||
.action(async (options: program.OptionValues) => {
|
|
||||||
await this.exitIfNotAuthed();
|
|
||||||
const command = new TestCommand(this.main.syncService, this.main.i18nService);
|
|
||||||
const response = await command.run(options);
|
|
||||||
this.processResponse(response);
|
|
||||||
});
|
|
||||||
|
|
||||||
program
|
|
||||||
.command('sync')
|
|
||||||
.description('Sync the directory.')
|
|
||||||
.on('--help', () => {
|
|
||||||
writeLn('\n Examples:');
|
|
||||||
writeLn('');
|
|
||||||
writeLn(' bwdc sync');
|
|
||||||
writeLn('', true);
|
|
||||||
})
|
|
||||||
.action(async () => {
|
|
||||||
await this.exitIfNotAuthed();
|
|
||||||
const command = new SyncCommand(this.main.syncService, this.main.i18nService);
|
|
||||||
const response = await command.run();
|
|
||||||
this.processResponse(response);
|
|
||||||
});
|
|
||||||
|
|
||||||
program
|
|
||||||
.command('last-sync <object>')
|
|
||||||
.description('Get the last successful sync date.')
|
|
||||||
.on('--help', () => {
|
|
||||||
writeLn('\n Notes:');
|
|
||||||
writeLn('');
|
|
||||||
writeLn(' Returns empty response if no sync has been performed for the given object.');
|
|
||||||
writeLn('');
|
|
||||||
writeLn(' Examples:');
|
|
||||||
writeLn('');
|
|
||||||
writeLn(' bwdc last-sync groups');
|
|
||||||
writeLn(' bwdc last-sync users');
|
|
||||||
writeLn('', true);
|
|
||||||
})
|
|
||||||
.action(async (object: string) => {
|
|
||||||
await this.exitIfNotAuthed();
|
|
||||||
const command = new LastSyncCommand(this.main.configurationService);
|
|
||||||
const response = await command.run(object);
|
|
||||||
this.processResponse(response);
|
|
||||||
});
|
|
||||||
|
|
||||||
program
|
|
||||||
.command('config <setting> [value]')
|
|
||||||
.description('Configure settings.')
|
|
||||||
.option('--secretenv <variable-name>', 'Read secret from the named environment variable.')
|
|
||||||
.option('--secretfile <filename>', 'Read secret from first line of the named file.')
|
|
||||||
.on('--help', () => {
|
|
||||||
writeLn('\n Settings:');
|
|
||||||
writeLn('');
|
|
||||||
writeLn(' server - On-premise hosted installation URL.');
|
|
||||||
writeLn(' directory - The type of directory to use.');
|
|
||||||
writeLn(' ldap.password - The password for connection to this LDAP server.');
|
|
||||||
writeLn(' azure.key - The Azure AD secret key.');
|
|
||||||
writeLn(' gsuite.key - The G Suite private key.');
|
|
||||||
writeLn(' okta.token - The Okta token.');
|
|
||||||
writeLn(' onelogin.secret - The OneLogin client secret.');
|
|
||||||
writeLn('');
|
|
||||||
writeLn(' Examples:');
|
|
||||||
writeLn('');
|
|
||||||
writeLn(' bwdc config server https://bw.company.com');
|
|
||||||
writeLn(' bwdc config server bitwarden.com');
|
|
||||||
writeLn(' bwdc config directory 1');
|
|
||||||
writeLn(' bwdc config ldap.password <password>');
|
|
||||||
writeLn(' bwdc config ldap.password --secretenv LDAP_PWD');
|
|
||||||
writeLn(' bwdc config azure.key <key>');
|
|
||||||
writeLn(' bwdc config gsuite.key <key>');
|
|
||||||
writeLn(' bwdc config okta.token <token>');
|
|
||||||
writeLn(' bwdc config onelogin.secret <secret>');
|
|
||||||
writeLn('', true);
|
|
||||||
})
|
|
||||||
.action(async (setting: string, value: string, options: program.OptionValues) => {
|
|
||||||
const command = new ConfigCommand(this.main.environmentService, this.main.i18nService,
|
|
||||||
this.main.configurationService);
|
|
||||||
const response = await command.run(setting, value, options);
|
|
||||||
this.processResponse(response);
|
|
||||||
});
|
|
||||||
|
|
||||||
program
|
|
||||||
.command('data-file')
|
|
||||||
.description('Path to data.json database file.')
|
|
||||||
.on('--help', () => {
|
|
||||||
writeLn('\n Examples:');
|
|
||||||
writeLn('');
|
|
||||||
writeLn(' bwdc data-file');
|
|
||||||
writeLn('', true);
|
|
||||||
})
|
|
||||||
.action(() => {
|
|
||||||
this.processResponse(
|
|
||||||
Response.success(new StringResponse(path.join(this.main.dataFilePath, 'data.json'))));
|
|
||||||
});
|
|
||||||
|
|
||||||
program
|
|
||||||
.command('clear-cache')
|
|
||||||
.description('Clear the sync cache.')
|
|
||||||
.on('--help', () => {
|
|
||||||
writeLn('\n Examples:');
|
|
||||||
writeLn('');
|
|
||||||
writeLn(' bwdc clear-cache');
|
|
||||||
writeLn('', true);
|
|
||||||
})
|
|
||||||
.action(async (options: program.OptionValues) => {
|
|
||||||
const command = new ClearCacheCommand(this.main.configurationService, this.main.i18nService);
|
|
||||||
const response = await command.run(options);
|
|
||||||
this.processResponse(response);
|
|
||||||
});
|
|
||||||
|
|
||||||
program
|
|
||||||
.command('update')
|
|
||||||
.description('Check for updates.')
|
|
||||||
.on('--help', () => {
|
|
||||||
writeLn('\n Notes:');
|
|
||||||
writeLn('');
|
|
||||||
writeLn(' Returns the URL to download the newest version of this CLI tool.');
|
|
||||||
writeLn('');
|
|
||||||
writeLn(' Use the `--raw` option to return only the download URL for the update.');
|
|
||||||
writeLn('');
|
|
||||||
writeLn(' Examples:');
|
|
||||||
writeLn('');
|
|
||||||
writeLn(' bwdc update');
|
|
||||||
writeLn(' bwdc update --raw');
|
|
||||||
writeLn('', true);
|
|
||||||
})
|
|
||||||
.action(async () => {
|
|
||||||
const command = new UpdateCommand(this.main.platformUtilsService, this.main.i18nService,
|
|
||||||
'directory-connector', 'bwdc', false);
|
|
||||||
const response = await command.run();
|
|
||||||
this.processResponse(response);
|
|
||||||
});
|
|
||||||
|
|
||||||
program
|
|
||||||
.parse(process.argv);
|
|
||||||
|
|
||||||
if (process.argv.slice(2).length === 0) {
|
|
||||||
program.outputHelp();
|
|
||||||
}
|
}
|
||||||
}
|
if (!Utils.isNullOrWhitespace(clientSecret)) {
|
||||||
|
process.env.BW_CLIENTSECRET = clientSecret;
|
||||||
async exitIfAuthed() {
|
|
||||||
const authed = await this.apiKeyService.isAuthenticated();
|
|
||||||
if (authed) {
|
|
||||||
const type = await this.apiKeyService.getEntityType();
|
|
||||||
const id = await this.apiKeyService.getEntityId();
|
|
||||||
this.processResponse(Response.error('You are already logged in as ' + type + '.' + id + '.'), true);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
async exitIfNotAuthed() {
|
options = Object.assign(options ?? {}, { apikey: true }); // force apikey use
|
||||||
const authed = await this.apiKeyService.isAuthenticated();
|
const response = await command.run(null, null, options);
|
||||||
if (!authed) {
|
this.processResponse(response);
|
||||||
this.processResponse(Response.error('You are not logged in.'), true);
|
});
|
||||||
}
|
|
||||||
|
program
|
||||||
|
.command("logout")
|
||||||
|
.description("Log out of the current user account.")
|
||||||
|
.on("--help", () => {
|
||||||
|
writeLn("\n Examples:");
|
||||||
|
writeLn("");
|
||||||
|
writeLn(" bwdc logout");
|
||||||
|
writeLn("", true);
|
||||||
|
})
|
||||||
|
.action(async () => {
|
||||||
|
await this.exitIfNotAuthed();
|
||||||
|
const command = new LogoutCommand(
|
||||||
|
this.main.authService,
|
||||||
|
this.main.i18nService,
|
||||||
|
async () => await this.main.logout()
|
||||||
|
);
|
||||||
|
const response = await command.run();
|
||||||
|
this.processResponse(response);
|
||||||
|
});
|
||||||
|
|
||||||
|
program
|
||||||
|
.command("test")
|
||||||
|
.description("Test a simulated sync.")
|
||||||
|
.option("-l, --last", "Since the last successful sync.")
|
||||||
|
.on("--help", () => {
|
||||||
|
writeLn("\n Examples:");
|
||||||
|
writeLn("");
|
||||||
|
writeLn(" bwdc test");
|
||||||
|
writeLn(" bwdc test --last");
|
||||||
|
writeLn("", true);
|
||||||
|
})
|
||||||
|
.action(async (options: program.OptionValues) => {
|
||||||
|
await this.exitIfNotAuthed();
|
||||||
|
const command = new TestCommand(this.main.syncService, this.main.i18nService);
|
||||||
|
const response = await command.run(options);
|
||||||
|
this.processResponse(response);
|
||||||
|
});
|
||||||
|
|
||||||
|
program
|
||||||
|
.command("sync")
|
||||||
|
.description("Sync the directory.")
|
||||||
|
.on("--help", () => {
|
||||||
|
writeLn("\n Examples:");
|
||||||
|
writeLn("");
|
||||||
|
writeLn(" bwdc sync");
|
||||||
|
writeLn("", true);
|
||||||
|
})
|
||||||
|
.action(async () => {
|
||||||
|
await this.exitIfNotAuthed();
|
||||||
|
const command = new SyncCommand(this.main.syncService, this.main.i18nService);
|
||||||
|
const response = await command.run();
|
||||||
|
this.processResponse(response);
|
||||||
|
});
|
||||||
|
|
||||||
|
program
|
||||||
|
.command("last-sync <object>")
|
||||||
|
.description("Get the last successful sync date.")
|
||||||
|
.on("--help", () => {
|
||||||
|
writeLn("\n Notes:");
|
||||||
|
writeLn("");
|
||||||
|
writeLn(" Returns empty response if no sync has been performed for the given object.");
|
||||||
|
writeLn("");
|
||||||
|
writeLn(" Examples:");
|
||||||
|
writeLn("");
|
||||||
|
writeLn(" bwdc last-sync groups");
|
||||||
|
writeLn(" bwdc last-sync users");
|
||||||
|
writeLn("", true);
|
||||||
|
})
|
||||||
|
.action(async (object: string) => {
|
||||||
|
await this.exitIfNotAuthed();
|
||||||
|
const command = new LastSyncCommand(this.main.stateService);
|
||||||
|
const response = await command.run(object);
|
||||||
|
this.processResponse(response);
|
||||||
|
});
|
||||||
|
|
||||||
|
program
|
||||||
|
.command("config <setting> [value]")
|
||||||
|
.description("Configure settings.")
|
||||||
|
.option("--secretenv <variable-name>", "Read secret from the named environment variable.")
|
||||||
|
.option("--secretfile <filename>", "Read secret from first line of the named file.")
|
||||||
|
.on("--help", () => {
|
||||||
|
writeLn("\n Settings:");
|
||||||
|
writeLn("");
|
||||||
|
writeLn(" server - On-premise hosted installation URL.");
|
||||||
|
writeLn(" directory - The type of directory to use.");
|
||||||
|
writeLn(" ldap.password - The password for connection to this LDAP server.");
|
||||||
|
writeLn(" azure.key - The Azure AD secret key.");
|
||||||
|
writeLn(" gsuite.key - The G Suite private key.");
|
||||||
|
writeLn(" okta.token - The Okta token.");
|
||||||
|
writeLn(" onelogin.secret - The OneLogin client secret.");
|
||||||
|
writeLn("");
|
||||||
|
writeLn(" Examples:");
|
||||||
|
writeLn("");
|
||||||
|
writeLn(" bwdc config server https://bw.company.com");
|
||||||
|
writeLn(" bwdc config server bitwarden.com");
|
||||||
|
writeLn(" bwdc config directory 1");
|
||||||
|
writeLn(" bwdc config ldap.password <password>");
|
||||||
|
writeLn(" bwdc config ldap.password --secretenv LDAP_PWD");
|
||||||
|
writeLn(" bwdc config azure.key <key>");
|
||||||
|
writeLn(" bwdc config gsuite.key <key>");
|
||||||
|
writeLn(" bwdc config okta.token <token>");
|
||||||
|
writeLn(" bwdc config onelogin.secret <secret>");
|
||||||
|
writeLn("", true);
|
||||||
|
})
|
||||||
|
.action(async (setting: string, value: string, options: program.OptionValues) => {
|
||||||
|
const command = new ConfigCommand(
|
||||||
|
this.main.environmentService,
|
||||||
|
this.main.i18nService,
|
||||||
|
this.main.stateService
|
||||||
|
);
|
||||||
|
const response = await command.run(setting, value, options);
|
||||||
|
this.processResponse(response);
|
||||||
|
});
|
||||||
|
|
||||||
|
program
|
||||||
|
.command("data-file")
|
||||||
|
.description("Path to data.json database file.")
|
||||||
|
.on("--help", () => {
|
||||||
|
writeLn("\n Examples:");
|
||||||
|
writeLn("");
|
||||||
|
writeLn(" bwdc data-file");
|
||||||
|
writeLn("", true);
|
||||||
|
})
|
||||||
|
.action(() => {
|
||||||
|
this.processResponse(
|
||||||
|
Response.success(new StringResponse(path.join(this.main.dataFilePath, "data.json")))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
program
|
||||||
|
.command("clear-cache")
|
||||||
|
.description("Clear the sync cache.")
|
||||||
|
.on("--help", () => {
|
||||||
|
writeLn("\n Examples:");
|
||||||
|
writeLn("");
|
||||||
|
writeLn(" bwdc clear-cache");
|
||||||
|
writeLn("", true);
|
||||||
|
})
|
||||||
|
.action(async (options: program.OptionValues) => {
|
||||||
|
const command = new ClearCacheCommand(this.main.i18nService, this.main.stateService);
|
||||||
|
const response = await command.run(options);
|
||||||
|
this.processResponse(response);
|
||||||
|
});
|
||||||
|
|
||||||
|
program
|
||||||
|
.command("update")
|
||||||
|
.description("Check for updates.")
|
||||||
|
.on("--help", () => {
|
||||||
|
writeLn("\n Notes:");
|
||||||
|
writeLn("");
|
||||||
|
writeLn(" Returns the URL to download the newest version of this CLI tool.");
|
||||||
|
writeLn("");
|
||||||
|
writeLn(" Use the `--raw` option to return only the download URL for the update.");
|
||||||
|
writeLn("");
|
||||||
|
writeLn(" Examples:");
|
||||||
|
writeLn("");
|
||||||
|
writeLn(" bwdc update");
|
||||||
|
writeLn(" bwdc update --raw");
|
||||||
|
writeLn("", true);
|
||||||
|
})
|
||||||
|
.action(async () => {
|
||||||
|
const command = new UpdateCommand(
|
||||||
|
this.main.platformUtilsService,
|
||||||
|
this.main.i18nService,
|
||||||
|
"directory-connector",
|
||||||
|
"bwdc",
|
||||||
|
false
|
||||||
|
);
|
||||||
|
const response = await command.run();
|
||||||
|
this.processResponse(response);
|
||||||
|
});
|
||||||
|
|
||||||
|
program.parse(process.argv);
|
||||||
|
|
||||||
|
if (process.argv.slice(2).length === 0) {
|
||||||
|
program.outputHelp();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async exitIfAuthed() {
|
||||||
|
const authed = await this.stateService.getIsAuthenticated();
|
||||||
|
if (authed) {
|
||||||
|
const type = await this.stateService.getEntityType();
|
||||||
|
const id = await this.stateService.getEntityId();
|
||||||
|
this.processResponse(
|
||||||
|
Response.error("You are already logged in as " + type + "." + id + "."),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async exitIfNotAuthed() {
|
||||||
|
const authed = await this.stateService.getIsAuthenticated();
|
||||||
|
if (!authed) {
|
||||||
|
this.processResponse(Response.error("You are not logged in."), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
30
src/scss/bootstrap.scss
vendored
30
src/scss/bootstrap.scss
vendored
@@ -1,5 +1,15 @@
|
|||||||
$theme-colors: ( "primary": #175DDC, "primary-accent": #1252A3, "danger": #dd4b39, "success": #00a65a, "info": #555555, "warning": #bf7e16, "secondary": #ced4da, "secondary-alt": #1A3B66);
|
$theme-colors: (
|
||||||
$font-family-sans-serif: 'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
|
"primary": #175ddc,
|
||||||
|
"primary-accent": #1252a3,
|
||||||
|
"danger": #dd4b39,
|
||||||
|
"success": #00a65a,
|
||||||
|
"info": #555555,
|
||||||
|
"warning": #bf7e16,
|
||||||
|
"secondary": #ced4da,
|
||||||
|
"secondary-alt": #1a3b66,
|
||||||
|
);
|
||||||
|
$font-family-sans-serif: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif,
|
||||||
|
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||||
|
|
||||||
$h1-font-size: 2rem;
|
$h1-font-size: 2rem;
|
||||||
$h2-font-size: 1.3rem;
|
$h2-font-size: 1.3rem;
|
||||||
@@ -8,13 +18,13 @@ $h4-font-size: 1rem;
|
|||||||
$h5-font-size: 1rem;
|
$h5-font-size: 1rem;
|
||||||
$h6-font-size: 1rem;
|
$h6-font-size: 1rem;
|
||||||
|
|
||||||
$primary: map_get($theme-colors, 'primary');
|
$primary: map_get($theme-colors, "primary");
|
||||||
$primary-accent: map_get($theme-colors, 'primary-accent');
|
$primary-accent: map_get($theme-colors, "primary-accent");
|
||||||
$success: map_get($theme-colors, 'success');
|
$success: map_get($theme-colors, "success");
|
||||||
$info: map_get($theme-colors, 'info');
|
$info: map_get($theme-colors, "info");
|
||||||
$warning: map_get($theme-colors, 'warning');
|
$warning: map_get($theme-colors, "warning");
|
||||||
$danger: map_get($theme-colors, 'danger');
|
$danger: map_get($theme-colors, "danger");
|
||||||
$secondary: map_get($theme-colors, 'secondary');
|
$secondary: map_get($theme-colors, "secondary");
|
||||||
$secondary-alt: map_get($theme-colors, 'secondary-alt');
|
$secondary-alt: map_get($theme-colors, "secondary-alt");
|
||||||
|
|
||||||
@import "~bootstrap/scss/bootstrap.scss";
|
@import "~bootstrap/scss/bootstrap.scss";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
@import "~bootstrap/scss/_variables.scss";
|
@import "~bootstrap/scss/_variables.scss";
|
||||||
|
|
||||||
html.os_windows {
|
html.os_windows {
|
||||||
body {
|
body {
|
||||||
border-top: 1px solid $gray-400;
|
border-top: 1px solid $gray-400;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,144 +1,143 @@
|
|||||||
@import "~bootstrap/scss/_variables.scss";
|
@import "~bootstrap/scss/_variables.scss";
|
||||||
|
|
||||||
body {
|
body {
|
||||||
padding: 10px 0 20px 0;
|
padding: 10px 0 20px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
border-bottom: 1px solid $border-color;
|
border-bottom: 1px solid $border-color;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
|
||||||
small {
|
small {
|
||||||
color: $text-muted;
|
color: $text-muted;
|
||||||
font-size: $h1-font-size * .5;
|
font-size: $h1-font-size * 0.5;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
h4 {
|
h4 {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
#duo-frame {
|
#duo-frame {
|
||||||
background: url('../images/loading.svg') 0 0 no-repeat;
|
background: url("../images/loading.svg") 0 0 no-repeat;
|
||||||
height: 380px;
|
height: 380px;
|
||||||
|
|
||||||
iframe {
|
iframe {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
app-root > #loading {
|
app-root > #loading {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
color: $text-muted;
|
color: $text-muted;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul.testing-list {
|
ul.testing-list {
|
||||||
ul {
|
ul {
|
||||||
padding-left: 18px;
|
padding-left: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
li.deleted {
|
li.deleted {
|
||||||
text-decoration: line-through;
|
text-decoration: line-through;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.callout {
|
.callout {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
border: 1px solid #000000;
|
border: 1px solid #000000;
|
||||||
border-left-width: 5px;
|
border-left-width: 5px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
border-color: #ddd;
|
border-color: #ddd;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
|
|
||||||
|
.callout-heading {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3.callout-heading {
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.callout-primary {
|
||||||
|
border-left-color: $primary;
|
||||||
|
|
||||||
.callout-heading {
|
.callout-heading {
|
||||||
margin-top: 0;
|
color: $primary;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
h3.callout-heading {
|
&.callout-info {
|
||||||
font-weight: bold;
|
border-left-color: $info;
|
||||||
text-transform: uppercase;
|
|
||||||
|
.callout-heading {
|
||||||
|
color: $info;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.callout-primary {
|
&.callout-danger {
|
||||||
border-left-color: $primary;
|
border-left-color: $danger;
|
||||||
|
|
||||||
.callout-heading {
|
.callout-heading {
|
||||||
color: $primary;
|
color: $danger;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.callout-info {
|
&.callout-success {
|
||||||
border-left-color: $info;
|
border-left-color: $success;
|
||||||
|
|
||||||
.callout-heading {
|
.callout-heading {
|
||||||
color: $info;
|
color: $success;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.callout-danger {
|
&.callout-warning {
|
||||||
border-left-color: $danger;
|
border-left-color: $warning;
|
||||||
|
|
||||||
.callout-heading {
|
.callout-heading {
|
||||||
color: $danger;
|
color: $warning;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.callout-success {
|
ul {
|
||||||
border-left-color: $success;
|
padding-left: 40px;
|
||||||
|
margin: 0;
|
||||||
.callout-heading {
|
}
|
||||||
color: $success;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.callout-warning {
|
|
||||||
border-left-color: $warning;
|
|
||||||
|
|
||||||
.callout-heading {
|
|
||||||
color: $warning;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
padding-left: 40px;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn[class*="btn-outline-"] {
|
.btn[class*="btn-outline-"] {
|
||||||
&:not(:hover) {
|
&:not(:hover) {
|
||||||
border-color: $secondary;
|
border-color: $secondary;
|
||||||
background-color: #fbfbfb;
|
background-color: #fbfbfb;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-outline-secondary {
|
.btn-outline-secondary {
|
||||||
color: $text-muted;
|
color: $text-muted;
|
||||||
|
|
||||||
&:hover:not(:disabled) {
|
&:hover:not(:disabled) {
|
||||||
color: $body-color;
|
color: $body-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:disabled {
|
&:disabled {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus,
|
&:focus,
|
||||||
&.focus {
|
&.focus {
|
||||||
box-shadow: 0 0 0 $btn-focus-width rgba(mix(color-yiq($primary), $primary, 15%), .5);
|
box-shadow: 0 0 0 $btn-focus-width rgba(mix(color-yiq($primary), $primary, 15%), 0.5);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,130 +1,126 @@
|
|||||||
$fa-font-path: "~font-awesome/fonts";
|
@import "~ngx-toastr/toastr";
|
||||||
@import "~font-awesome/scss/font-awesome.scss";
|
|
||||||
@import "~angular2-toaster/toaster";
|
|
||||||
|
|
||||||
@import "~bootstrap/scss/_variables.scss";
|
@import "~bootstrap/scss/_variables.scss";
|
||||||
|
|
||||||
#toast-container {
|
.toast-container {
|
||||||
|
.toast-close-button {
|
||||||
|
font-size: 18px;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ngx-toastr {
|
||||||
|
align-items: center;
|
||||||
|
background-image: none !important;
|
||||||
|
border-radius: $border-radius;
|
||||||
|
box-shadow: 0 0 8px rgba(0, 0, 0, 0.35);
|
||||||
|
display: flex;
|
||||||
|
padding: 15px;
|
||||||
|
|
||||||
.toast-close-button {
|
.toast-close-button {
|
||||||
right: -0.15em;
|
position: absolute;
|
||||||
|
right: 5px;
|
||||||
|
top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toast {
|
&:hover {
|
||||||
opacity: 1 !important;
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.6);
|
||||||
background-image: none !important;
|
|
||||||
border-radius: $border-radius;
|
|
||||||
box-shadow: 0 0 8px rgba(0, 0, 0, 0.35);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.6);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:before {
|
|
||||||
font-family: FontAwesome;
|
|
||||||
font-size: 25px;
|
|
||||||
line-height: 20px;
|
|
||||||
float: left;
|
|
||||||
color: #ffffff;
|
|
||||||
padding-right: 10px;
|
|
||||||
margin: auto 0 auto -36px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toaster-icon {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toast-message {
|
|
||||||
p {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.toast-danger, &.toast-error {
|
|
||||||
background-image: none !important;
|
|
||||||
background-color: $danger;
|
|
||||||
|
|
||||||
&:before {
|
|
||||||
content: "\f0e7";
|
|
||||||
margin-left: -30px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.toast-warning {
|
|
||||||
background-image: none !important;
|
|
||||||
background-color: $warning;
|
|
||||||
|
|
||||||
&:before {
|
|
||||||
content: "\f071";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.toast-info {
|
|
||||||
background-image: none !important;
|
|
||||||
background-color: $info;
|
|
||||||
|
|
||||||
&:before {
|
|
||||||
content: "\f05a";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.toast-success {
|
|
||||||
background-image: none !important;
|
|
||||||
background-color: $success;
|
|
||||||
|
|
||||||
&:before {
|
|
||||||
content: "\f00C";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon i::before {
|
||||||
|
float: left;
|
||||||
|
font-style: normal;
|
||||||
|
font-family: $icomoon-font-family;
|
||||||
|
font-size: 25px;
|
||||||
|
line-height: 20px;
|
||||||
|
padding-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-message {
|
||||||
|
p {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.toast-danger,
|
||||||
|
&.toast-error {
|
||||||
|
background-color: $danger;
|
||||||
|
|
||||||
|
.icon i::before {
|
||||||
|
content: map_get($icons, "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.toast-warning {
|
||||||
|
background-color: $warning;
|
||||||
|
|
||||||
|
.icon i::before {
|
||||||
|
content: map_get($icons, "exclamation-triangle");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.toast-info {
|
||||||
|
background-color: $info;
|
||||||
|
|
||||||
|
.icon i:before {
|
||||||
|
content: map_get($icons, "info-circle");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.toast-success {
|
||||||
|
background-color: $success;
|
||||||
|
|
||||||
|
.icon i:before {
|
||||||
|
content: map_get($icons, "check");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes modalshow {
|
@keyframes modalshow {
|
||||||
0% {
|
0% {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translate(0, -25%);
|
transform: translate(0, -25%);
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translate(0, 0);
|
transform: translate(0, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes backdropshow {
|
@keyframes backdropshow {
|
||||||
0% {
|
0% {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
opacity: $modal-backdrop-opacity;
|
opacity: $modal-backdrop-opacity;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal {
|
.modal {
|
||||||
display: block !important;
|
display: block !important;
|
||||||
opacity: 1 !important;
|
opacity: 1 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-dialog {
|
.modal-dialog {
|
||||||
.modal.fade & {
|
.modal.fade & {
|
||||||
transform: initial !important;
|
transform: initial !important;
|
||||||
animation: modalshow 0.3s ease-in;
|
animation: modalshow 0.3s ease-in;
|
||||||
}
|
}
|
||||||
.modal.show & {
|
.modal.show & {
|
||||||
transform: initial !important;
|
transform: initial !important;
|
||||||
}
|
}
|
||||||
transform: translate(0, 0);
|
transform: translate(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-backdrop {
|
.modal-backdrop {
|
||||||
&.fade {
|
&.fade {
|
||||||
animation: backdropshow 0.1s ease-in;
|
animation: backdropshow 0.1s ease-in;
|
||||||
}
|
}
|
||||||
opacity: $modal-backdrop-opacity !important;
|
opacity: $modal-backdrop-opacity !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
@import "../../jslib/angular/src/scss/webfonts.css";
|
@import "../../jslib/angular/src/scss/webfonts.css";
|
||||||
|
@import "../../jslib/angular/src/scss/bwicons/styles/style.scss";
|
||||||
@import "bootstrap.scss";
|
@import "bootstrap.scss";
|
||||||
@import "pages.scss";
|
@import "pages.scss";
|
||||||
@import "misc.scss";
|
@import "misc.scss";
|
||||||
|
|||||||
@@ -1,32 +1,36 @@
|
|||||||
import { ApiKeyService } from 'jslib-common/abstractions/apiKey.service';
|
import { AuthService } from "jslib-common/abstractions/auth.service";
|
||||||
import { AuthService } from 'jslib-common/abstractions/auth.service';
|
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
|
||||||
import { EnvironmentService } from 'jslib-common/abstractions/environment.service';
|
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
|
import { TokenService } from "jslib-common/abstractions/token.service";
|
||||||
import { TokenService } from 'jslib-common/abstractions/token.service';
|
import { StateService } from "../abstractions/state.service";
|
||||||
|
|
||||||
import { ApiService as ApiServiceBase } from 'jslib-common/services/api.service';
|
import { ApiService as ApiServiceBase } from "jslib-common/services/api.service";
|
||||||
|
|
||||||
export async function refreshToken(apiKeyService: ApiKeyService, authService: AuthService) {
|
export async function refreshToken(stateService: StateService, authService: AuthService) {
|
||||||
try {
|
try {
|
||||||
const clientId = await apiKeyService.getClientId();
|
const clientId = await stateService.getApiKeyClientId();
|
||||||
const clientSecret = await apiKeyService.getClientSecret();
|
const clientSecret = await stateService.getApiKeyClientSecret();
|
||||||
if (clientId != null && clientSecret != null) {
|
if (clientId != null && clientSecret != null) {
|
||||||
await authService.logInApiKey(clientId, clientSecret);
|
await authService.logInApiKey(clientId, clientSecret);
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
return Promise.reject(e);
|
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return Promise.reject(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ApiService extends ApiServiceBase {
|
export class ApiService extends ApiServiceBase {
|
||||||
constructor(tokenService: TokenService, platformUtilsService: PlatformUtilsService, environmentService: EnvironmentService,
|
constructor(
|
||||||
private refreshTokenCallback: () => Promise<void>, logoutCallback: (expired: boolean) => Promise<void>,
|
tokenService: TokenService,
|
||||||
customUserAgent: string = null) {
|
platformUtilsService: PlatformUtilsService,
|
||||||
super(tokenService, platformUtilsService, environmentService, logoutCallback, customUserAgent);
|
environmentService: EnvironmentService,
|
||||||
this.apiKeyRefresh = (clientId: string, clientSecret: string) => this.doRefreshToken();
|
private refreshTokenCallback: () => Promise<void>,
|
||||||
}
|
logoutCallback: (expired: boolean) => Promise<void>,
|
||||||
|
customUserAgent: string = null
|
||||||
|
) {
|
||||||
|
super(tokenService, platformUtilsService, environmentService, logoutCallback, customUserAgent);
|
||||||
|
}
|
||||||
|
|
||||||
doRefreshToken(): Promise<void> {
|
doRefreshToken(): Promise<void> {
|
||||||
return this.refreshTokenCallback();
|
return this.refreshTokenCallback();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,61 +1,119 @@
|
|||||||
import { ApiService } from 'jslib-common/abstractions/api.service';
|
import { ApiService } from "jslib-common/abstractions/api.service";
|
||||||
import { ApiKeyService } from 'jslib-common/abstractions/apiKey.service';
|
import { AppIdService } from "jslib-common/abstractions/appId.service";
|
||||||
import { AppIdService } from 'jslib-common/abstractions/appId.service';
|
import { CryptoService } from "jslib-common/abstractions/crypto.service";
|
||||||
import { CryptoService } from 'jslib-common/abstractions/crypto.service';
|
import { CryptoFunctionService } from "jslib-common/abstractions/cryptoFunction.service";
|
||||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
|
||||||
import { LogService } from 'jslib-common/abstractions/log.service';
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
import { MessagingService } from 'jslib-common/abstractions/messaging.service';
|
import { KeyConnectorService } from "jslib-common/abstractions/keyConnector.service";
|
||||||
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
|
import { LogService } from "jslib-common/abstractions/log.service";
|
||||||
import { TokenService } from 'jslib-common/abstractions/token.service';
|
import { MessagingService } from "jslib-common/abstractions/messaging.service";
|
||||||
import { UserService } from 'jslib-common/abstractions/user.service';
|
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.service';
|
import { TokenService } from "jslib-common/abstractions/token.service";
|
||||||
|
import { VaultTimeoutService } from "jslib-common/abstractions/vaultTimeout.service";
|
||||||
|
import { StateService } from "../abstractions/state.service";
|
||||||
|
|
||||||
import { AuthService as AuthServiceBase } from 'jslib-common/services/auth.service';
|
import { AuthService as AuthServiceBase } from "jslib-common/services/auth.service";
|
||||||
|
|
||||||
import { AuthResult } from 'jslib-common/models/domain/authResult';
|
import { Account, DirectoryConfigurations, DirectorySettings } from "src/models/account";
|
||||||
import { DeviceRequest } from 'jslib-common/models/request/deviceRequest';
|
|
||||||
import { TokenRequest } from 'jslib-common/models/request/tokenRequest';
|
import { AccountKeys, AccountProfile, AccountTokens } from "jslib-common/models/domain/account";
|
||||||
import { IdentityTokenResponse } from 'jslib-common/models/response/identityTokenResponse';
|
import { AuthResult } from "jslib-common/models/domain/authResult";
|
||||||
|
|
||||||
|
import { DeviceRequest } from "jslib-common/models/request/deviceRequest";
|
||||||
|
import { TokenRequest } from "jslib-common/models/request/tokenRequest";
|
||||||
|
|
||||||
|
import { IdentityTokenResponse } from "jslib-common/models/response/identityTokenResponse";
|
||||||
|
|
||||||
export class AuthService extends AuthServiceBase {
|
export class AuthService extends AuthServiceBase {
|
||||||
|
constructor(
|
||||||
|
cryptoService: CryptoService,
|
||||||
|
apiService: ApiService,
|
||||||
|
tokenService: TokenService,
|
||||||
|
appIdService: AppIdService,
|
||||||
|
i18nService: I18nService,
|
||||||
|
platformUtilsService: PlatformUtilsService,
|
||||||
|
messagingService: MessagingService,
|
||||||
|
vaultTimeoutService: VaultTimeoutService,
|
||||||
|
logService: LogService,
|
||||||
|
cryptoFunctionService: CryptoFunctionService,
|
||||||
|
environmentService: EnvironmentService,
|
||||||
|
keyConnectorService: KeyConnectorService,
|
||||||
|
stateService: StateService
|
||||||
|
) {
|
||||||
|
super(
|
||||||
|
cryptoService,
|
||||||
|
apiService,
|
||||||
|
tokenService,
|
||||||
|
appIdService,
|
||||||
|
i18nService,
|
||||||
|
platformUtilsService,
|
||||||
|
messagingService,
|
||||||
|
vaultTimeoutService,
|
||||||
|
logService,
|
||||||
|
cryptoFunctionService,
|
||||||
|
keyConnectorService,
|
||||||
|
environmentService,
|
||||||
|
stateService,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
constructor(cryptoService: CryptoService, apiService: ApiService, userService: UserService,
|
async logInApiKey(clientId: string, clientSecret: string): Promise<AuthResult> {
|
||||||
tokenService: TokenService, appIdService: AppIdService, i18nService: I18nService,
|
this.selectedTwoFactorProviderType = null;
|
||||||
platformUtilsService: PlatformUtilsService, messagingService: MessagingService,
|
if (clientId.startsWith("organization")) {
|
||||||
vaultTimeoutService: VaultTimeoutService, logService: LogService, private apiKeyService: ApiKeyService,
|
return await this.organizationLogInHelper(clientId, clientSecret);
|
||||||
setCryptoKeys = true) {
|
|
||||||
super(cryptoService, apiService, userService, tokenService, appIdService, i18nService, platformUtilsService,
|
|
||||||
messagingService, vaultTimeoutService, logService, setCryptoKeys);
|
|
||||||
}
|
}
|
||||||
|
return await super.logInApiKey(clientId, clientSecret);
|
||||||
|
}
|
||||||
|
|
||||||
async logInApiKey(clientId: string, clientSecret: string): Promise<AuthResult> {
|
private async organizationLogInHelper(clientId: string, clientSecret: string) {
|
||||||
this.selectedTwoFactorProviderType = null;
|
const appId = await this.appIdService.getAppId();
|
||||||
if (clientId.startsWith('organization')) {
|
const entityId = clientId.split("organization.")[1];
|
||||||
return await this.organizationLogInHelper(clientId, clientSecret);
|
const deviceRequest = new DeviceRequest(appId, this.platformUtilsService);
|
||||||
}
|
const request = new TokenRequest(
|
||||||
return await super.logInApiKey(clientId, clientSecret);
|
null,
|
||||||
}
|
null,
|
||||||
|
[clientId, clientSecret],
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
deviceRequest
|
||||||
|
);
|
||||||
|
|
||||||
async logOut(callback: Function) {
|
const response = await this.apiService.postIdentityToken(request);
|
||||||
this.apiKeyService.clear();
|
const result = new AuthResult();
|
||||||
super.logOut(callback);
|
result.twoFactor = !(response as any).accessToken;
|
||||||
}
|
|
||||||
|
|
||||||
private async organizationLogInHelper(clientId: string, clientSecret: string) {
|
const tokenResponse = response as IdentityTokenResponse;
|
||||||
const appId = await this.appIdService.getAppId();
|
result.resetMasterPassword = tokenResponse.resetMasterPassword;
|
||||||
const deviceRequest = new DeviceRequest(appId, this.platformUtilsService);
|
await this.stateService.addAccount(
|
||||||
const request = new TokenRequest(null, null, [clientId, clientSecret], null,
|
new Account({
|
||||||
null, false, null, deviceRequest);
|
profile: {
|
||||||
|
...new AccountProfile(),
|
||||||
const response = await this.apiService.postIdentityToken(request);
|
...{
|
||||||
const result = new AuthResult();
|
userId: entityId,
|
||||||
result.twoFactor = !(response as any).accessToken;
|
apiKeyClientId: clientId,
|
||||||
|
entityId: entityId,
|
||||||
const tokenResponse = response as IdentityTokenResponse;
|
},
|
||||||
result.resetMasterPassword = tokenResponse.resetMasterPassword;
|
},
|
||||||
await this.tokenService.setToken(tokenResponse.accessToken);
|
tokens: {
|
||||||
await this.apiKeyService.setInformation(clientId, clientSecret);
|
...new AccountTokens(),
|
||||||
|
...{
|
||||||
return result;
|
accessToken: tokenResponse.accessToken,
|
||||||
}
|
refreshToken: tokenResponse.refreshToken,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
keys: {
|
||||||
|
...new AccountKeys(),
|
||||||
|
...{
|
||||||
|
apiKeyClientSecret: clientSecret,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
directorySettings: new DirectorySettings(),
|
||||||
|
directoryConfigurations: new DirectoryConfigurations(),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,464 +1,529 @@
|
|||||||
import * as graph from '@microsoft/microsoft-graph-client';
|
import * as graph from "@microsoft/microsoft-graph-client";
|
||||||
import * as graphType from '@microsoft/microsoft-graph-types';
|
import * as graphType from "@microsoft/microsoft-graph-types";
|
||||||
import * as https from 'https';
|
import * as https from "https";
|
||||||
import * as querystring from 'querystring';
|
import * as querystring from "querystring";
|
||||||
|
|
||||||
import { DirectoryType } from '../enums/directoryType';
|
import { DirectoryType } from "../enums/directoryType";
|
||||||
|
|
||||||
import { AzureConfiguration } from '../models/azureConfiguration';
|
import { AzureConfiguration } from "../models/azureConfiguration";
|
||||||
import { GroupEntry } from '../models/groupEntry';
|
import { GroupEntry } from "../models/groupEntry";
|
||||||
import { SyncConfiguration } from '../models/syncConfiguration';
|
import { SyncConfiguration } from "../models/syncConfiguration";
|
||||||
import { UserEntry } from '../models/userEntry';
|
import { UserEntry } from "../models/userEntry";
|
||||||
|
|
||||||
import { BaseDirectoryService } from './baseDirectory.service';
|
import { BaseDirectoryService } from "./baseDirectory.service";
|
||||||
import { ConfigurationService } from './configuration.service';
|
import { IDirectoryService } from "./directory.service";
|
||||||
import { IDirectoryService } from './directory.service';
|
|
||||||
|
|
||||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
import { LogService } from 'jslib-common/abstractions/log.service';
|
import { LogService } from "jslib-common/abstractions/log.service";
|
||||||
|
import { StateService } from "../abstractions/state.service";
|
||||||
|
|
||||||
const NextLink = '@odata.nextLink';
|
const AzurePublicIdentityAuhtority = "login.microsoftonline.com";
|
||||||
const DeltaLink = '@odata.deltaLink';
|
const AzureGovermentIdentityAuhtority = "login.microsoftonline.us";
|
||||||
const ObjectType = '@odata.type';
|
|
||||||
const UserSelectParams = '?$select=id,mail,userPrincipalName,displayName,accountEnabled';
|
const NextLink = "@odata.nextLink";
|
||||||
|
const DeltaLink = "@odata.deltaLink";
|
||||||
|
const ObjectType = "@odata.type";
|
||||||
|
const UserSelectParams = "?$select=id,mail,userPrincipalName,displayName,accountEnabled";
|
||||||
|
|
||||||
enum UserSetType {
|
enum UserSetType {
|
||||||
IncludeUser,
|
IncludeUser,
|
||||||
ExcludeUser,
|
ExcludeUser,
|
||||||
IncludeGroup,
|
IncludeGroup,
|
||||||
ExcludeGroup,
|
ExcludeGroup,
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AzureDirectoryService extends BaseDirectoryService implements IDirectoryService {
|
export class AzureDirectoryService extends BaseDirectoryService implements IDirectoryService {
|
||||||
private client: graph.Client;
|
private client: graph.Client;
|
||||||
private dirConfig: AzureConfiguration;
|
private dirConfig: AzureConfiguration;
|
||||||
private syncConfig: SyncConfiguration;
|
private syncConfig: SyncConfiguration;
|
||||||
private accessToken: string;
|
private accessToken: string;
|
||||||
private accessTokenExpiration: Date;
|
private accessTokenExpiration: Date;
|
||||||
|
|
||||||
constructor(private configurationService: ConfigurationService, private logService: LogService,
|
constructor(
|
||||||
private i18nService: I18nService) {
|
private logService: LogService,
|
||||||
super();
|
private i18nService: I18nService,
|
||||||
this.init();
|
private stateService: StateService
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> {
|
||||||
|
const type = await this.stateService.getDirectoryType();
|
||||||
|
if (type !== DirectoryType.AzureActiveDirectory) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> {
|
this.dirConfig = await this.stateService.getDirectory<AzureConfiguration>(
|
||||||
const type = await this.configurationService.getDirectoryType();
|
DirectoryType.AzureActiveDirectory
|
||||||
if (type !== DirectoryType.AzureActiveDirectory) {
|
);
|
||||||
return;
|
if (this.dirConfig == null) {
|
||||||
}
|
return;
|
||||||
|
|
||||||
this.dirConfig = await this.configurationService.getDirectory<AzureConfiguration>(
|
|
||||||
DirectoryType.AzureActiveDirectory);
|
|
||||||
if (this.dirConfig == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.syncConfig = await this.configurationService.getSync();
|
|
||||||
if (this.syncConfig == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let users: UserEntry[];
|
|
||||||
if (this.syncConfig.users) {
|
|
||||||
users = await this.getCurrentUsers();
|
|
||||||
const deletedUsers = await this.getDeletedUsers(force, !test);
|
|
||||||
users = users.concat(deletedUsers);
|
|
||||||
}
|
|
||||||
|
|
||||||
let groups: GroupEntry[];
|
|
||||||
if (this.syncConfig.groups) {
|
|
||||||
const setFilter = await this.createAadCustomSet(this.syncConfig.groupFilter);
|
|
||||||
groups = await this.getGroups(setFilter);
|
|
||||||
users = this.filterUsersFromGroupsSet(users, groups, setFilter, this.syncConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
return [groups, users];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getCurrentUsers(): Promise<UserEntry[]> {
|
this.syncConfig = await this.stateService.getSync();
|
||||||
const entryIds = new Set<string>();
|
if (this.syncConfig == null) {
|
||||||
const entries: UserEntry[] = [];
|
return;
|
||||||
const userReq = this.client.api('/users' + UserSelectParams);
|
|
||||||
let res = await userReq.get();
|
|
||||||
const setFilter = this.createCustomUserSet(this.syncConfig.userFilter);
|
|
||||||
while (true) {
|
|
||||||
const users: graphType.User[] = res.value;
|
|
||||||
if (users != null) {
|
|
||||||
for (const user of users) {
|
|
||||||
if (user.id == null || entryIds.has(user.id)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const entry = this.buildUser(user);
|
|
||||||
if (await this.filterOutUserResult(setFilter, entry, true)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!entry.disabled && !entry.deleted &&
|
|
||||||
(entry.email == null || entry.email.indexOf('#') > -1)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
entries.push(entry);
|
|
||||||
entryIds.add(user.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (res[NextLink] == null) {
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
const nextReq = this.client.api(res[NextLink]);
|
|
||||||
res = await nextReq.get();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return entries;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getDeletedUsers(force: boolean, saveDelta: boolean): Promise<UserEntry[]> {
|
let users: UserEntry[];
|
||||||
const entryIds = new Set<string>();
|
if (this.syncConfig.users) {
|
||||||
const entries: UserEntry[] = [];
|
users = await this.getCurrentUsers();
|
||||||
|
const deletedUsers = await this.getDeletedUsers(force, !test);
|
||||||
let res: any = null;
|
users = users.concat(deletedUsers);
|
||||||
const token = await this.configurationService.getUserDeltaToken();
|
|
||||||
if (!force && token != null) {
|
|
||||||
try {
|
|
||||||
const deltaReq = this.client.api(token);
|
|
||||||
res = await deltaReq.get();
|
|
||||||
} catch {
|
|
||||||
res = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (res == null) {
|
|
||||||
const userReq = this.client.api('/users/delta' + UserSelectParams);
|
|
||||||
res = await userReq.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
const setFilter = this.createCustomUserSet(this.syncConfig.userFilter);
|
|
||||||
while (true) {
|
|
||||||
const users: graphType.User[] = res.value;
|
|
||||||
if (users != null) {
|
|
||||||
for (const user of users) {
|
|
||||||
if (user.id == null || entryIds.has(user.id)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const entry = this.buildUser(user);
|
|
||||||
if (!entry.deleted) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (await this.filterOutUserResult(setFilter, entry, false)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
entries.push(entry);
|
|
||||||
entryIds.add(user.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (res[NextLink] == null) {
|
|
||||||
if (res[DeltaLink] != null && saveDelta) {
|
|
||||||
await this.configurationService.saveUserDeltaToken(res[DeltaLink]);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
const nextReq = this.client.api(res[NextLink]);
|
|
||||||
res = await nextReq.get();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return entries;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createAadCustomSet(filter: string): Promise<[boolean, Set<string>]> {
|
let groups: GroupEntry[];
|
||||||
if (filter == null || filter === '') {
|
if (this.syncConfig.groups) {
|
||||||
return null;
|
const setFilter = await this.createAadCustomSet(this.syncConfig.groupFilter);
|
||||||
}
|
groups = await this.getGroups(setFilter);
|
||||||
|
users = this.filterUsersFromGroupsSet(users, groups, setFilter, this.syncConfig);
|
||||||
const mainParts = filter.split('|');
|
|
||||||
if (mainParts.length < 1 || mainParts[0] == null || mainParts[0].trim() === '') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const parts = mainParts[0].split(':');
|
|
||||||
if (parts.length !== 2) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const keyword = parts[0].trim().toLowerCase();
|
|
||||||
let exclude = true;
|
|
||||||
if (keyword === 'include') {
|
|
||||||
exclude = false;
|
|
||||||
} else if (keyword === 'exclude') {
|
|
||||||
exclude = true;
|
|
||||||
} else if (keyword === 'excludeadministrativeunit') {
|
|
||||||
exclude = true;
|
|
||||||
} else if (keyword === 'includeadministrativeunit') {
|
|
||||||
exclude = false;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const set = new Set<string>();
|
|
||||||
const pieces = parts[1].split(',');
|
|
||||||
if (keyword === 'excludeadministrativeunit' || keyword === 'includeadministrativeunit') {
|
|
||||||
for (const p of pieces) {
|
|
||||||
const auMembers = await this.client
|
|
||||||
.api(`https://graph.microsoft.com/v1.0/directory/administrativeUnits/${p}/members`).get();
|
|
||||||
for (const auMember of auMembers.value) {
|
|
||||||
if (auMember['@odata.type'] === '#microsoft.graph.group') {
|
|
||||||
set.add(auMember.displayName.toLowerCase());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (const p of pieces) {
|
|
||||||
set.add(p.trim().toLowerCase());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return [exclude, set];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private createCustomUserSet(filter: string): [UserSetType, Set<string>] {
|
return [groups, users];
|
||||||
if (filter == null || filter === '') {
|
}
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mainParts = filter.split('|');
|
private async getCurrentUsers(): Promise<UserEntry[]> {
|
||||||
if (mainParts.length < 1 || mainParts[0] == null || mainParts[0].trim() === '') {
|
const entryIds = new Set<string>();
|
||||||
return null;
|
let entries: UserEntry[] = [];
|
||||||
}
|
let users: graphType.User[];
|
||||||
|
const setFilter = this.createCustomUserSet(this.syncConfig.userFilter);
|
||||||
|
const userIdsToExclude = new Set<string>();
|
||||||
|
|
||||||
const parts = mainParts[0].split(':');
|
// Only get users for the groups provided in includeGroup filter
|
||||||
if (parts.length !== 2) {
|
if (setFilter != null && setFilter[0] === UserSetType.IncludeGroup) {
|
||||||
return null;
|
users = await this.getUsersByGroups(setFilter);
|
||||||
}
|
// Get the users in the excludedGroups and filter them out from all users
|
||||||
|
} else if (setFilter != null && setFilter[0] === UserSetType.ExcludeGroup) {
|
||||||
|
(await this.getUsersByGroups(setFilter)).forEach((user: graphType.User) =>
|
||||||
|
userIdsToExclude.add(user.id)
|
||||||
|
);
|
||||||
|
const userReq = this.client.api("/users" + UserSelectParams);
|
||||||
|
users = await this.getUsersByResource(userReq);
|
||||||
|
} else {
|
||||||
|
const userReq = this.client.api("/users" + UserSelectParams);
|
||||||
|
users = await this.getUsersByResource(userReq);
|
||||||
|
}
|
||||||
|
if (users != null) {
|
||||||
|
entries = await this.buildUserEntries(users, userIdsToExclude, setFilter);
|
||||||
|
}
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
const keyword = parts[0].trim().toLowerCase();
|
private async getDeletedUsers(force: boolean, saveDelta: boolean): Promise<UserEntry[]> {
|
||||||
let userSetType = UserSetType.IncludeUser;
|
const entryIds = new Set<string>();
|
||||||
if (keyword === 'include') {
|
const entries: UserEntry[] = [];
|
||||||
userSetType = UserSetType.IncludeUser;
|
|
||||||
} else if (keyword === 'exclude') {
|
|
||||||
userSetType = UserSetType.ExcludeUser;
|
|
||||||
} else if (keyword === 'includegroup') {
|
|
||||||
userSetType = UserSetType.IncludeGroup;
|
|
||||||
} else if (keyword === 'excludegroup') {
|
|
||||||
userSetType = UserSetType.ExcludeGroup;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const set = new Set<string>();
|
let res: any = null;
|
||||||
const pieces = parts[1].split(',');
|
const token = await this.stateService.getUserDelta();
|
||||||
for (const p of pieces) {
|
if (!force && token != null) {
|
||||||
set.add(p.trim().toLowerCase());
|
try {
|
||||||
}
|
const deltaReq = this.client.api(token);
|
||||||
|
res = await deltaReq.get();
|
||||||
return [userSetType, set];
|
} catch {
|
||||||
|
res = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async filterOutUserResult(setFilter: [UserSetType, Set<string>], user: UserEntry,
|
if (res == null) {
|
||||||
checkGroupsFilter: boolean): Promise<boolean> {
|
const userReq = this.client.api("/users/delta" + UserSelectParams);
|
||||||
if (setFilter == null) {
|
res = await userReq.get();
|
||||||
return false;
|
}
|
||||||
|
|
||||||
|
const setFilter = this.createCustomUserSet(this.syncConfig.userFilter);
|
||||||
|
while (true) {
|
||||||
|
const users: graphType.User[] = res.value;
|
||||||
|
if (users != null) {
|
||||||
|
for (const user of users) {
|
||||||
|
if (user.id == null || entryIds.has(user.id)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const entry = this.buildUser(user);
|
||||||
|
if (!entry.deleted) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
setFilter != null &&
|
||||||
|
(setFilter[0] === UserSetType.IncludeUser ||
|
||||||
|
setFilter[0] === UserSetType.ExcludeUser) &&
|
||||||
|
(await this.filterOutUserResult(setFilter, entry))
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.push(entry);
|
||||||
|
entryIds.add(user.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res[NextLink] == null) {
|
||||||
|
if (res[DeltaLink] != null && saveDelta) {
|
||||||
|
await this.stateService.setUserDelta(res[DeltaLink]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
const nextReq = this.client.api(res[NextLink]);
|
||||||
|
res = await nextReq.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createAadCustomSet(filter: string): Promise<[boolean, Set<string>]> {
|
||||||
|
if (filter == null || filter === "") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mainParts = filter.split("|");
|
||||||
|
if (mainParts.length < 1 || mainParts[0] == null || mainParts[0].trim() === "") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = mainParts[0].split(":");
|
||||||
|
if (parts.length !== 2) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyword = parts[0].trim().toLowerCase();
|
||||||
|
let exclude = true;
|
||||||
|
if (keyword === "include") {
|
||||||
|
exclude = false;
|
||||||
|
} else if (keyword === "exclude") {
|
||||||
|
exclude = true;
|
||||||
|
} else if (keyword === "excludeadministrativeunit") {
|
||||||
|
exclude = true;
|
||||||
|
} else if (keyword === "includeadministrativeunit") {
|
||||||
|
exclude = false;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const set = new Set<string>();
|
||||||
|
const pieces = parts[1].split(",");
|
||||||
|
if (keyword === "excludeadministrativeunit" || keyword === "includeadministrativeunit") {
|
||||||
|
for (const p of pieces) {
|
||||||
|
const auMembers = await this.client
|
||||||
|
.api(`https://graph.microsoft.com/v1.0/directory/administrativeUnits/${p}/members`)
|
||||||
|
.get();
|
||||||
|
for (const auMember of auMembers.value) {
|
||||||
|
if (auMember["@odata.type"] === "#microsoft.graph.group") {
|
||||||
|
set.add(auMember.displayName.toLowerCase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const p of pieces) {
|
||||||
|
set.add(p.trim().toLowerCase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [exclude, set];
|
||||||
|
}
|
||||||
|
|
||||||
|
private createCustomUserSet(filter: string): [UserSetType, Set<string>] {
|
||||||
|
if (filter == null || filter === "") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mainParts = filter.split("|");
|
||||||
|
if (mainParts.length < 1 || mainParts[0] == null || mainParts[0].trim() === "") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = mainParts[0].split(":");
|
||||||
|
if (parts.length !== 2) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyword = parts[0].trim().toLowerCase();
|
||||||
|
let userSetType = UserSetType.IncludeUser;
|
||||||
|
if (keyword === "include") {
|
||||||
|
userSetType = UserSetType.IncludeUser;
|
||||||
|
} else if (keyword === "exclude") {
|
||||||
|
userSetType = UserSetType.ExcludeUser;
|
||||||
|
} else if (keyword === "includegroup") {
|
||||||
|
userSetType = UserSetType.IncludeGroup;
|
||||||
|
} else if (keyword === "excludegroup") {
|
||||||
|
userSetType = UserSetType.ExcludeGroup;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const set = new Set<string>();
|
||||||
|
const pieces = parts[1].split(",");
|
||||||
|
for (const p of pieces) {
|
||||||
|
set.add(p.trim().toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
return [userSetType, set];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async filterOutUserResult(
|
||||||
|
setFilter: [UserSetType, Set<string>],
|
||||||
|
user: UserEntry
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (setFilter == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let userSetTypeExclude = null;
|
||||||
|
if (setFilter[0] === UserSetType.IncludeUser) {
|
||||||
|
userSetTypeExclude = false;
|
||||||
|
} else if (setFilter[0] === UserSetType.ExcludeUser) {
|
||||||
|
userSetTypeExclude = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userSetTypeExclude != null) {
|
||||||
|
return this.filterOutResult([userSetTypeExclude, setFilter[1]], user.email);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildUser(user: graphType.User): UserEntry {
|
||||||
|
const entry = new UserEntry();
|
||||||
|
entry.referenceId = user.id;
|
||||||
|
entry.externalId = user.id;
|
||||||
|
entry.email = user.mail;
|
||||||
|
|
||||||
|
if (
|
||||||
|
user.userPrincipalName &&
|
||||||
|
(entry.email == null || entry.email === "" || entry.email.indexOf("onmicrosoft.com") > -1)
|
||||||
|
) {
|
||||||
|
entry.email = user.userPrincipalName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.email != null) {
|
||||||
|
entry.email = entry.email.trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.disabled = user.accountEnabled == null ? false : !user.accountEnabled;
|
||||||
|
|
||||||
|
if ((user as any)["@removed"] != null && (user as any)["@removed"].reason === "changed") {
|
||||||
|
entry.deleted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getGroups(setFilter: [boolean, Set<string>]): Promise<GroupEntry[]> {
|
||||||
|
const entryIds = new Set<string>();
|
||||||
|
const entries: GroupEntry[] = [];
|
||||||
|
const groupsReq = this.client.api("/groups");
|
||||||
|
let res = await groupsReq.get();
|
||||||
|
while (true) {
|
||||||
|
const groups: graphType.Group[] = res.value;
|
||||||
|
if (groups != null) {
|
||||||
|
for (const group of groups) {
|
||||||
|
if (group.id == null || entryIds.has(group.id)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (this.filterOutResult(setFilter, group.displayName)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entry = await this.buildGroup(group);
|
||||||
|
entries.push(entry);
|
||||||
|
entryIds.add(group.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res[NextLink] == null) {
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
const nextReq = this.client.api(res[NextLink]);
|
||||||
|
res = await nextReq.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getUsersByResource(usersRequest: graph.GraphRequest) {
|
||||||
|
const users: graphType.User[] = [];
|
||||||
|
let res = await usersRequest.get();
|
||||||
|
res.value.forEach((user: graphType.User) => users.push(user));
|
||||||
|
while (res[NextLink] != null) {
|
||||||
|
const nextReq = this.client.api(res[NextLink]);
|
||||||
|
res = await nextReq.get();
|
||||||
|
res.value.forEach((user: graphType.User) => users.push(user));
|
||||||
|
}
|
||||||
|
return users;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getUsersByGroups(setFilter: [UserSetType, Set<string>]): Promise<graphType.User[]> {
|
||||||
|
const users: graphType.User[] = [];
|
||||||
|
for (const group of setFilter[1]) {
|
||||||
|
const groupUsersReq = this.client.api(
|
||||||
|
`/groups/${group}/transitiveMembers` + UserSelectParams
|
||||||
|
);
|
||||||
|
users.push(...(await this.getUsersByResource(groupUsersReq)));
|
||||||
|
}
|
||||||
|
return users;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async buildUserEntries(
|
||||||
|
users: graphType.User[],
|
||||||
|
userIdsToExclude: Set<string>,
|
||||||
|
setFilter: [UserSetType, Set<string>]
|
||||||
|
) {
|
||||||
|
const entryIds = new Set<string>();
|
||||||
|
const entries: UserEntry[] = [];
|
||||||
|
|
||||||
|
for (const user of users) {
|
||||||
|
if (user.id == null || entryIds.has(user.id) || userIdsToExclude.has(user.id)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const entry = this.buildUser(user);
|
||||||
|
|
||||||
|
if (
|
||||||
|
setFilter != null &&
|
||||||
|
(setFilter[0] === UserSetType.IncludeUser || setFilter[0] === UserSetType.ExcludeUser) &&
|
||||||
|
(await this.filterOutUserResult(setFilter, entry))
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!this.isInvalidUser(entry)) {
|
||||||
|
entries.push(entry);
|
||||||
|
entryIds.add(user.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
private isInvalidUser(user: UserEntry): boolean {
|
||||||
|
return !user.disabled && !user.deleted && (user.email == null || user.email.indexOf("#") > -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async buildGroup(group: graphType.Group): Promise<GroupEntry> {
|
||||||
|
const entry = new GroupEntry();
|
||||||
|
entry.referenceId = group.id;
|
||||||
|
entry.externalId = group.id;
|
||||||
|
entry.name = group.displayName;
|
||||||
|
|
||||||
|
const memReq = this.client.api("/groups/" + group.id + "/members");
|
||||||
|
let memRes = await memReq.get();
|
||||||
|
while (true) {
|
||||||
|
const members: any = memRes.value;
|
||||||
|
if (members != null) {
|
||||||
|
for (const member of members) {
|
||||||
|
if (member[ObjectType] === "#microsoft.graph.group") {
|
||||||
|
entry.groupMemberReferenceIds.add((member as graphType.Group).id);
|
||||||
|
} else if (member[ObjectType] === "#microsoft.graph.user") {
|
||||||
|
entry.userMemberExternalIds.add((member as graphType.User).id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (memRes[NextLink] == null) {
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
const nextMemReq = this.client.api(memRes[NextLink]);
|
||||||
|
memRes = await nextMemReq.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
private init() {
|
||||||
|
this.client = graph.Client.init({
|
||||||
|
authProvider: (done) => {
|
||||||
|
if (
|
||||||
|
this.dirConfig.applicationId == null ||
|
||||||
|
this.dirConfig.key == null ||
|
||||||
|
this.dirConfig.tenant == null
|
||||||
|
) {
|
||||||
|
done(new Error(this.i18nService.t("dirConfigIncomplete")), null);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let userSetTypeExclude = null;
|
const identityAuthority =
|
||||||
if (setFilter[0] === UserSetType.IncludeUser) {
|
this.dirConfig.identityAuthority != null
|
||||||
userSetTypeExclude = false;
|
? this.dirConfig.identityAuthority
|
||||||
} else if (setFilter[0] === UserSetType.ExcludeUser) {
|
: AzurePublicIdentityAuhtority;
|
||||||
userSetTypeExclude = true;
|
if (
|
||||||
|
identityAuthority !== AzurePublicIdentityAuhtority &&
|
||||||
|
identityAuthority !== AzureGovermentIdentityAuhtority
|
||||||
|
) {
|
||||||
|
done(new Error(this.i18nService.t("dirConfigIncomplete")), null);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userSetTypeExclude != null) {
|
if (!this.accessTokenIsExpired()) {
|
||||||
return this.filterOutResult([userSetTypeExclude, setFilter[1]], user.email);
|
done(null, this.accessToken);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We need to *not* call the /checkMemberGroups method for deleted users, it will always fail
|
this.accessToken = null;
|
||||||
if (!checkGroupsFilter) {
|
this.accessTokenExpiration = null;
|
||||||
return false;
|
|
||||||
}
|
const data = querystring.stringify({
|
||||||
const memberGroups = await this.client.api(`/users/${user.externalId}/checkMemberGroups`).post({
|
client_id: this.dirConfig.applicationId,
|
||||||
groupIds: Array.from(setFilter[1]),
|
client_secret: this.dirConfig.key,
|
||||||
|
grant_type: "client_credentials",
|
||||||
|
scope: "https://graph.microsoft.com/.default",
|
||||||
});
|
});
|
||||||
if (memberGroups.value.length > 0 && setFilter[0] === UserSetType.IncludeGroup) {
|
|
||||||
return false;
|
|
||||||
} else if (memberGroups.value.length > 0 && setFilter[0] === UserSetType.ExcludeGroup) {
|
|
||||||
return true;
|
|
||||||
} else if (memberGroups.value.length === 0 && setFilter[0] === UserSetType.IncludeGroup) {
|
|
||||||
return true;
|
|
||||||
} else if (memberGroups.value.length === 0 && setFilter[0] === UserSetType.ExcludeGroup) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
const req = https
|
||||||
}
|
.request(
|
||||||
|
{
|
||||||
private buildUser(user: graphType.User): UserEntry {
|
host: identityAuthority,
|
||||||
const entry = new UserEntry();
|
path: "/" + this.dirConfig.tenant + "/oauth2/v2.0/token",
|
||||||
entry.referenceId = user.id;
|
method: "POST",
|
||||||
entry.externalId = user.id;
|
headers: {
|
||||||
entry.email = user.mail;
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
"Content-Length": Buffer.byteLength(data),
|
||||||
if (user.userPrincipalName && (entry.email == null || entry.email === '' ||
|
},
|
||||||
entry.email.indexOf('onmicrosoft.com') > -1)) {
|
|
||||||
entry.email = user.userPrincipalName;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entry.email != null) {
|
|
||||||
entry.email = entry.email.trim().toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
entry.disabled = user.accountEnabled == null ? false : !user.accountEnabled;
|
|
||||||
|
|
||||||
if ((user as any)['@removed'] != null && (user as any)['@removed'].reason === 'changed') {
|
|
||||||
entry.deleted = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getGroups(setFilter: [boolean, Set<string>]): Promise<GroupEntry[]> {
|
|
||||||
const entryIds = new Set<string>();
|
|
||||||
const entries: GroupEntry[] = [];
|
|
||||||
const groupsReq = this.client.api('/groups');
|
|
||||||
let res = await groupsReq.get();
|
|
||||||
while (true) {
|
|
||||||
const groups: graphType.Group[] = res.value;
|
|
||||||
if (groups != null) {
|
|
||||||
for (const group of groups) {
|
|
||||||
if (group.id == null || entryIds.has(group.id)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (this.filterOutResult(setFilter, group.displayName)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const entry = await this.buildGroup(group);
|
|
||||||
entries.push(entry);
|
|
||||||
entryIds.add(group.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (res[NextLink] == null) {
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
const nextReq = this.client.api(res[NextLink]);
|
|
||||||
res = await nextReq.get();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return entries;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async buildGroup(group: graphType.Group): Promise<GroupEntry> {
|
|
||||||
const entry = new GroupEntry();
|
|
||||||
entry.referenceId = group.id;
|
|
||||||
entry.externalId = group.id;
|
|
||||||
entry.name = group.displayName;
|
|
||||||
|
|
||||||
const memReq = this.client.api('/groups/' + group.id + '/members');
|
|
||||||
let memRes = await memReq.get();
|
|
||||||
while (true) {
|
|
||||||
const members: any = memRes.value;
|
|
||||||
if (members != null) {
|
|
||||||
for (const member of members) {
|
|
||||||
if (member[ObjectType] === '#microsoft.graph.group') {
|
|
||||||
entry.groupMemberReferenceIds.add((member as graphType.Group).id);
|
|
||||||
} else if (member[ObjectType] === '#microsoft.graph.user') {
|
|
||||||
entry.userMemberExternalIds.add((member as graphType.User).id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (memRes[NextLink] == null) {
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
const nextMemReq = this.client.api(memRes[NextLink]);
|
|
||||||
memRes = await nextMemReq.get();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
private init() {
|
|
||||||
this.client = graph.Client.init({
|
|
||||||
authProvider: done => {
|
|
||||||
if (this.dirConfig.applicationId == null || this.dirConfig.key == null ||
|
|
||||||
this.dirConfig.tenant == null) {
|
|
||||||
done(new Error(this.i18nService.t('dirConfigIncomplete')), null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.accessTokenIsExpired()) {
|
|
||||||
done(null, this.accessToken);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.accessToken = null;
|
|
||||||
this.accessTokenExpiration = null;
|
|
||||||
|
|
||||||
const data = querystring.stringify({
|
|
||||||
client_id: this.dirConfig.applicationId,
|
|
||||||
client_secret: this.dirConfig.key,
|
|
||||||
grant_type: 'client_credentials',
|
|
||||||
scope: 'https://graph.microsoft.com/.default',
|
|
||||||
});
|
|
||||||
|
|
||||||
const req = https.request({
|
|
||||||
host: 'login.microsoftonline.com',
|
|
||||||
path: '/' + this.dirConfig.tenant + '/oauth2/v2.0/token',
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
|
||||||
'Content-Length': Buffer.byteLength(data),
|
|
||||||
},
|
|
||||||
}, res => {
|
|
||||||
res.setEncoding('utf8');
|
|
||||||
res.on('data', (chunk: string) => {
|
|
||||||
const d = JSON.parse(chunk);
|
|
||||||
if (res.statusCode === 200 && d.access_token != null) {
|
|
||||||
this.setAccessTokenExpiration(d.access_token, d.expires_in);
|
|
||||||
done(null, d.access_token);
|
|
||||||
} else if (d.error != null && d.error_description != null) {
|
|
||||||
const shortError = d.error_description?.split('\n', 1)[0];
|
|
||||||
const err = new Error(d.error + ' (' + res.statusCode + '): ' + shortError);
|
|
||||||
// tslint: disable-next-line
|
|
||||||
console.error(d.error_description);
|
|
||||||
done(err, null);
|
|
||||||
} else {
|
|
||||||
const err = new Error('Unknown error (' + res.statusCode + ').');
|
|
||||||
done(err, null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}).on('error', err => {
|
|
||||||
done(err, null);
|
|
||||||
});
|
|
||||||
|
|
||||||
req.write(data);
|
|
||||||
req.end();
|
|
||||||
},
|
},
|
||||||
});
|
(res) => {
|
||||||
|
res.setEncoding("utf8");
|
||||||
|
res.on("data", (chunk: string) => {
|
||||||
|
const d = JSON.parse(chunk);
|
||||||
|
if (res.statusCode === 200 && d.access_token != null) {
|
||||||
|
this.setAccessTokenExpiration(d.access_token, d.expires_in);
|
||||||
|
done(null, d.access_token);
|
||||||
|
} else if (d.error != null && d.error_description != null) {
|
||||||
|
const shortError = d.error_description?.split("\n", 1)[0];
|
||||||
|
const err = new Error(d.error + " (" + res.statusCode + "): " + shortError);
|
||||||
|
// tslint:disable-next-line
|
||||||
|
console.error(d.error_description);
|
||||||
|
done(err, null);
|
||||||
|
} else {
|
||||||
|
const err = new Error("Unknown error (" + res.statusCode + ").");
|
||||||
|
done(err, null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.on("error", (err) => {
|
||||||
|
done(err, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
req.write(data);
|
||||||
|
req.end();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private accessTokenIsExpired() {
|
||||||
|
if (this.accessToken == null || this.accessTokenExpiration == null) {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private accessTokenIsExpired() {
|
// expired if less than 2 minutes til expiration
|
||||||
if (this.accessToken == null || this.accessTokenExpiration == null) {
|
const now = new Date();
|
||||||
return true;
|
return this.accessTokenExpiration.getTime() - now.getTime() < 120000;
|
||||||
}
|
}
|
||||||
|
|
||||||
// expired if less than 2 minutes til expiration
|
private setAccessTokenExpiration(accessToken: string, expSeconds: number) {
|
||||||
const now = new Date();
|
if (accessToken == null || expSeconds == null) {
|
||||||
return this.accessTokenExpiration.getTime() - now.getTime() < 120000;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
private setAccessTokenExpiration(accessToken: string, expSeconds: number) {
|
this.accessToken = accessToken;
|
||||||
if (accessToken == null || expSeconds == null) {
|
const exp = new Date();
|
||||||
return;
|
exp.setSeconds(exp.getSeconds() + expSeconds);
|
||||||
}
|
this.accessTokenExpiration = exp;
|
||||||
|
}
|
||||||
this.accessToken = accessToken;
|
|
||||||
const exp = new Date();
|
|
||||||
exp.setSeconds(exp.getSeconds() + expSeconds);
|
|
||||||
this.accessTokenExpiration = exp;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,91 +1,95 @@
|
|||||||
import { SyncConfiguration } from '../models/syncConfiguration';
|
import { SyncConfiguration } from "../models/syncConfiguration";
|
||||||
|
|
||||||
import { GroupEntry } from '../models/groupEntry';
|
import { GroupEntry } from "../models/groupEntry";
|
||||||
import { UserEntry } from '../models/userEntry';
|
import { UserEntry } from "../models/userEntry";
|
||||||
|
|
||||||
export abstract class BaseDirectoryService {
|
export abstract class BaseDirectoryService {
|
||||||
protected createDirectoryQuery(filter: string) {
|
protected createDirectoryQuery(filter: string) {
|
||||||
if (filter == null || filter === '') {
|
if (filter == null || filter === "") {
|
||||||
return null;
|
return null;
|
||||||
}
|
|
||||||
|
|
||||||
const mainParts = filter.split('|');
|
|
||||||
if (mainParts.length < 2 || mainParts[1] == null || mainParts[1].trim() === '') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return mainParts[1].trim();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected createCustomSet(filter: string): [boolean, Set<string>] {
|
const mainParts = filter.split("|");
|
||||||
if (filter == null || filter === '') {
|
if (mainParts.length < 2 || mainParts[1] == null || mainParts[1].trim() === "") {
|
||||||
return null;
|
return null;
|
||||||
}
|
|
||||||
|
|
||||||
const mainParts = filter.split('|');
|
|
||||||
if (mainParts.length < 1 || mainParts[0] == null || mainParts[0].trim() === '') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const parts = mainParts[0].split(':');
|
|
||||||
if (parts.length !== 2) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const keyword = parts[0].trim().toLowerCase();
|
|
||||||
let exclude = true;
|
|
||||||
if (keyword === 'include') {
|
|
||||||
exclude = false;
|
|
||||||
} else if (keyword === 'exclude') {
|
|
||||||
exclude = true;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const set = new Set<string>();
|
|
||||||
const pieces = parts[1].split(',');
|
|
||||||
for (const p of pieces) {
|
|
||||||
set.add(p.trim().toLowerCase());
|
|
||||||
}
|
|
||||||
|
|
||||||
return [exclude, set];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected filterOutResult(setFilter: [boolean, Set<string>], result: string) {
|
return mainParts[1].trim();
|
||||||
if (setFilter != null) {
|
}
|
||||||
const cleanResult = result != null ? result.trim().toLowerCase() : '--';
|
|
||||||
const excluded = setFilter[0];
|
|
||||||
const set = setFilter[1];
|
|
||||||
|
|
||||||
if (excluded && set.has(cleanResult)) {
|
protected createCustomSet(filter: string): [boolean, Set<string>] {
|
||||||
return true;
|
if (filter == null || filter === "") {
|
||||||
} else if (!excluded && !set.has(cleanResult)) {
|
return null;
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected filterUsersFromGroupsSet(users: UserEntry[], groups: GroupEntry[],
|
const mainParts = filter.split("|");
|
||||||
setFilter: [boolean, Set<string>], syncConfig: SyncConfiguration): UserEntry[] {
|
if (mainParts.length < 1 || mainParts[0] == null || mainParts[0].trim() === "") {
|
||||||
if (setFilter == null || users == null) {
|
return null;
|
||||||
return users;
|
|
||||||
}
|
|
||||||
|
|
||||||
return users.filter(u => {
|
|
||||||
if (u.deleted) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (u.disabled && syncConfig.removeDisabled) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return groups.filter(g => g.userMemberExternalIds.has(u.externalId)).length > 0;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected forceGroup(force: boolean, users: UserEntry[]): boolean {
|
const parts = mainParts[0].split(":");
|
||||||
return force || (users != null && users.filter(u => !u.deleted && !u.disabled).length > 0);
|
if (parts.length !== 2) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const keyword = parts[0].trim().toLowerCase();
|
||||||
|
let exclude = true;
|
||||||
|
if (keyword === "include") {
|
||||||
|
exclude = false;
|
||||||
|
} else if (keyword === "exclude") {
|
||||||
|
exclude = true;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const set = new Set<string>();
|
||||||
|
const pieces = parts[1].split(",");
|
||||||
|
for (const p of pieces) {
|
||||||
|
set.add(p.trim().toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
return [exclude, set];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected filterOutResult(setFilter: [boolean, Set<string>], result: string) {
|
||||||
|
if (setFilter != null) {
|
||||||
|
const cleanResult = result != null ? result.trim().toLowerCase() : "--";
|
||||||
|
const excluded = setFilter[0];
|
||||||
|
const set = setFilter[1];
|
||||||
|
|
||||||
|
if (excluded && set.has(cleanResult)) {
|
||||||
|
return true;
|
||||||
|
} else if (!excluded && !set.has(cleanResult)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected filterUsersFromGroupsSet(
|
||||||
|
users: UserEntry[],
|
||||||
|
groups: GroupEntry[],
|
||||||
|
setFilter: [boolean, Set<string>],
|
||||||
|
syncConfig: SyncConfiguration
|
||||||
|
): UserEntry[] {
|
||||||
|
if (setFilter == null || users == null) {
|
||||||
|
return users;
|
||||||
|
}
|
||||||
|
|
||||||
|
return users.filter((u) => {
|
||||||
|
if (u.deleted) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (u.disabled && syncConfig.removeDisabled) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups.filter((g) => g.userMemberExternalIds.has(u.externalId)).length > 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected forceGroup(force: boolean, users: UserEntry[]): boolean {
|
||||||
|
return force || (users != null && users.filter((u) => !u.deleted && !u.disabled).length > 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,229 +0,0 @@
|
|||||||
import { DirectoryType } from '../enums/directoryType';
|
|
||||||
|
|
||||||
import { StorageService } from 'jslib-common/abstractions/storage.service';
|
|
||||||
import { AzureConfiguration } from '../models/azureConfiguration';
|
|
||||||
import { GSuiteConfiguration } from '../models/gsuiteConfiguration';
|
|
||||||
import { LdapConfiguration } from '../models/ldapConfiguration';
|
|
||||||
import { OktaConfiguration } from '../models/oktaConfiguration';
|
|
||||||
import { OneLoginConfiguration } from '../models/oneLoginConfiguration';
|
|
||||||
import { SyncConfiguration } from '../models/syncConfiguration';
|
|
||||||
|
|
||||||
const StoredSecurely = '[STORED SECURELY]';
|
|
||||||
const Keys = {
|
|
||||||
ldap: 'ldapPassword',
|
|
||||||
gsuite: 'gsuitePrivateKey',
|
|
||||||
azure: 'azureKey',
|
|
||||||
okta: 'oktaToken',
|
|
||||||
oneLogin: 'oneLoginClientSecret',
|
|
||||||
directoryConfigPrefix: 'directoryConfig_',
|
|
||||||
sync: 'syncConfig',
|
|
||||||
directoryType: 'directoryType',
|
|
||||||
userDelta: 'userDeltaToken',
|
|
||||||
groupDelta: 'groupDeltaToken',
|
|
||||||
lastUserSync: 'lastUserSync',
|
|
||||||
lastGroupSync: 'lastGroupSync',
|
|
||||||
lastSyncHash: 'lastSyncHash',
|
|
||||||
organizationId: 'organizationId',
|
|
||||||
};
|
|
||||||
|
|
||||||
export class ConfigurationService {
|
|
||||||
constructor(private storageService: StorageService, private secureStorageService: StorageService,
|
|
||||||
private useSecureStorageForSecrets = true) { }
|
|
||||||
|
|
||||||
async getDirectory<T>(type: DirectoryType): Promise<T> {
|
|
||||||
const config = await this.storageService.get<T>(Keys.directoryConfigPrefix + type);
|
|
||||||
if (config == null) {
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.useSecureStorageForSecrets) {
|
|
||||||
switch (type) {
|
|
||||||
case DirectoryType.Ldap:
|
|
||||||
(config as any).password = await this.secureStorageService.get<string>(Keys.ldap);
|
|
||||||
break;
|
|
||||||
case DirectoryType.AzureActiveDirectory:
|
|
||||||
(config as any).key = await this.secureStorageService.get<string>(Keys.azure);
|
|
||||||
break;
|
|
||||||
case DirectoryType.Okta:
|
|
||||||
(config as any).token = await this.secureStorageService.get<string>(Keys.okta);
|
|
||||||
break;
|
|
||||||
case DirectoryType.GSuite:
|
|
||||||
(config as any).privateKey = await this.secureStorageService.get<string>(Keys.gsuite);
|
|
||||||
break;
|
|
||||||
case DirectoryType.OneLogin:
|
|
||||||
(config as any).clientSecret = await this.secureStorageService.get<string>(Keys.oneLogin);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
|
|
||||||
async saveDirectory(type: DirectoryType,
|
|
||||||
config: LdapConfiguration | GSuiteConfiguration | AzureConfiguration | OktaConfiguration |
|
|
||||||
OneLoginConfiguration): Promise<any> {
|
|
||||||
const savedConfig: any = Object.assign({}, config);
|
|
||||||
if (this.useSecureStorageForSecrets) {
|
|
||||||
switch (type) {
|
|
||||||
case DirectoryType.Ldap:
|
|
||||||
if (savedConfig.password == null) {
|
|
||||||
await this.secureStorageService.remove(Keys.ldap);
|
|
||||||
} else {
|
|
||||||
await this.secureStorageService.save(Keys.ldap, savedConfig.password);
|
|
||||||
savedConfig.password = StoredSecurely;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case DirectoryType.AzureActiveDirectory:
|
|
||||||
if (savedConfig.key == null) {
|
|
||||||
await this.secureStorageService.remove(Keys.azure);
|
|
||||||
} else {
|
|
||||||
await this.secureStorageService.save(Keys.azure, savedConfig.key);
|
|
||||||
savedConfig.key = StoredSecurely;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case DirectoryType.Okta:
|
|
||||||
if (savedConfig.token == null) {
|
|
||||||
await this.secureStorageService.remove(Keys.okta);
|
|
||||||
} else {
|
|
||||||
await this.secureStorageService.save(Keys.okta, savedConfig.token);
|
|
||||||
savedConfig.token = StoredSecurely;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case DirectoryType.GSuite:
|
|
||||||
if (savedConfig.privateKey == null) {
|
|
||||||
await this.secureStorageService.remove(Keys.gsuite);
|
|
||||||
} else {
|
|
||||||
(config as GSuiteConfiguration).privateKey = savedConfig.privateKey =
|
|
||||||
savedConfig.privateKey.replace(/\\n/g, '\n');
|
|
||||||
await this.secureStorageService.save(Keys.gsuite, savedConfig.privateKey);
|
|
||||||
savedConfig.privateKey = StoredSecurely;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case DirectoryType.OneLogin:
|
|
||||||
if (savedConfig.clientSecret == null) {
|
|
||||||
await this.secureStorageService.remove(Keys.oneLogin);
|
|
||||||
} else {
|
|
||||||
await this.secureStorageService.save(Keys.oneLogin, savedConfig.clientSecret);
|
|
||||||
savedConfig.clientSecret = StoredSecurely;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await this.storageService.save(Keys.directoryConfigPrefix + type, savedConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
getSync(): Promise<SyncConfiguration> {
|
|
||||||
return this.storageService.get<SyncConfiguration>(Keys.sync);
|
|
||||||
}
|
|
||||||
|
|
||||||
saveSync(config: SyncConfiguration) {
|
|
||||||
return this.storageService.save(Keys.sync, config);
|
|
||||||
}
|
|
||||||
|
|
||||||
getDirectoryType(): Promise<DirectoryType> {
|
|
||||||
return this.storageService.get<DirectoryType>(Keys.directoryType);
|
|
||||||
}
|
|
||||||
|
|
||||||
async saveDirectoryType(type: DirectoryType) {
|
|
||||||
const currentType = await this.getDirectoryType();
|
|
||||||
if (type !== currentType) {
|
|
||||||
await this.clearStatefulSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.storageService.save(Keys.directoryType, type);
|
|
||||||
}
|
|
||||||
|
|
||||||
getUserDeltaToken(): Promise<string> {
|
|
||||||
return this.storageService.get<string>(Keys.userDelta);
|
|
||||||
}
|
|
||||||
|
|
||||||
saveUserDeltaToken(token: string) {
|
|
||||||
if (token == null) {
|
|
||||||
return this.storageService.remove(Keys.userDelta);
|
|
||||||
} else {
|
|
||||||
return this.storageService.save(Keys.userDelta, token);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getGroupDeltaToken(): Promise<string> {
|
|
||||||
return this.storageService.get<string>(Keys.groupDelta);
|
|
||||||
}
|
|
||||||
|
|
||||||
saveGroupDeltaToken(token: string) {
|
|
||||||
if (token == null) {
|
|
||||||
return this.storageService.remove(Keys.groupDelta);
|
|
||||||
} else {
|
|
||||||
return this.storageService.save(Keys.groupDelta, token);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getLastUserSyncDate(): Promise<Date> {
|
|
||||||
const dateString = await this.storageService.get<string>(Keys.lastUserSync);
|
|
||||||
if (dateString == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return new Date(dateString);
|
|
||||||
}
|
|
||||||
|
|
||||||
saveLastUserSyncDate(date: Date) {
|
|
||||||
if (date == null) {
|
|
||||||
return this.storageService.remove(Keys.lastUserSync);
|
|
||||||
} else {
|
|
||||||
return this.storageService.save(Keys.lastUserSync, date);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getLastGroupSyncDate(): Promise<Date> {
|
|
||||||
const dateString = await this.storageService.get<string>(Keys.lastGroupSync);
|
|
||||||
if (dateString == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return new Date(dateString);
|
|
||||||
}
|
|
||||||
|
|
||||||
saveLastGroupSyncDate(date: Date) {
|
|
||||||
if (date == null) {
|
|
||||||
return this.storageService.remove(Keys.lastGroupSync);
|
|
||||||
} else {
|
|
||||||
return this.storageService.save(Keys.lastGroupSync, date);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getLastSyncHash(): Promise<string> {
|
|
||||||
return this.storageService.get<string>(Keys.lastSyncHash);
|
|
||||||
}
|
|
||||||
|
|
||||||
saveLastSyncHash(hash: string) {
|
|
||||||
if (hash == null) {
|
|
||||||
return this.storageService.remove(Keys.lastSyncHash);
|
|
||||||
} else {
|
|
||||||
return this.storageService.save(Keys.lastSyncHash, hash);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getOrganizationId(): Promise<string> {
|
|
||||||
return this.storageService.get<string>(Keys.organizationId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async saveOrganizationId(id: string) {
|
|
||||||
const currentId = await this.getOrganizationId();
|
|
||||||
if (currentId !== id) {
|
|
||||||
await this.clearStatefulSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (id == null) {
|
|
||||||
return this.storageService.remove(Keys.organizationId);
|
|
||||||
} else {
|
|
||||||
return this.storageService.save(Keys.organizationId, id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async clearStatefulSettings(hashToo = false) {
|
|
||||||
await this.saveUserDeltaToken(null);
|
|
||||||
await this.saveGroupDeltaToken(null);
|
|
||||||
await this.saveLastGroupSyncDate(null);
|
|
||||||
await this.saveLastUserSyncDate(null);
|
|
||||||
if (hashToo) {
|
|
||||||
await this.saveLastSyncHash(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { GroupEntry } from '../models/groupEntry';
|
import { GroupEntry } from "../models/groupEntry";
|
||||||
import { UserEntry } from '../models/userEntry';
|
import { UserEntry } from "../models/userEntry";
|
||||||
|
|
||||||
export interface IDirectoryService {
|
export interface IDirectoryService {
|
||||||
getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]>;
|
getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,248 +1,260 @@
|
|||||||
import { JWT } from 'google-auth-library';
|
import { JWT } from "google-auth-library";
|
||||||
import {
|
import { admin_directory_v1, google } from "googleapis";
|
||||||
admin_directory_v1,
|
|
||||||
google,
|
|
||||||
} from 'googleapis';
|
|
||||||
|
|
||||||
import { DirectoryType } from '../enums/directoryType';
|
import { DirectoryType } from "../enums/directoryType";
|
||||||
|
|
||||||
import { GroupEntry } from '../models/groupEntry';
|
import { GroupEntry } from "../models/groupEntry";
|
||||||
import { GSuiteConfiguration } from '../models/gsuiteConfiguration';
|
import { GSuiteConfiguration } from "../models/gsuiteConfiguration";
|
||||||
import { SyncConfiguration } from '../models/syncConfiguration';
|
import { SyncConfiguration } from "../models/syncConfiguration";
|
||||||
import { UserEntry } from '../models/userEntry';
|
import { UserEntry } from "../models/userEntry";
|
||||||
|
|
||||||
import { BaseDirectoryService } from './baseDirectory.service';
|
import { BaseDirectoryService } from "./baseDirectory.service";
|
||||||
import { ConfigurationService } from './configuration.service';
|
import { IDirectoryService } from "./directory.service";
|
||||||
import { IDirectoryService } from './directory.service';
|
|
||||||
|
|
||||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
import { LogService } from 'jslib-common/abstractions/log.service';
|
import { LogService } from "jslib-common/abstractions/log.service";
|
||||||
|
import { StateService } from "../abstractions/state.service";
|
||||||
|
|
||||||
export class GSuiteDirectoryService extends BaseDirectoryService implements IDirectoryService {
|
export class GSuiteDirectoryService extends BaseDirectoryService implements IDirectoryService {
|
||||||
private client: JWT;
|
private client: JWT;
|
||||||
private service: admin_directory_v1.Admin;
|
private service: admin_directory_v1.Admin;
|
||||||
private authParams: any;
|
private authParams: any;
|
||||||
private dirConfig: GSuiteConfiguration;
|
private dirConfig: GSuiteConfiguration;
|
||||||
private syncConfig: SyncConfiguration;
|
private syncConfig: SyncConfiguration;
|
||||||
|
|
||||||
constructor(private configurationService: ConfigurationService, private logService: LogService,
|
constructor(
|
||||||
private i18nService: I18nService) {
|
private logService: LogService,
|
||||||
super();
|
private i18nService: I18nService,
|
||||||
this.service = google.admin('directory_v1');
|
private stateService: StateService
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
this.service = google.admin("directory_v1");
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> {
|
||||||
|
const type = await this.stateService.getDirectoryType();
|
||||||
|
if (type !== DirectoryType.GSuite) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> {
|
this.dirConfig = await this.stateService.getDirectory<GSuiteConfiguration>(
|
||||||
const type = await this.configurationService.getDirectoryType();
|
DirectoryType.GSuite
|
||||||
if (type !== DirectoryType.GSuite) {
|
);
|
||||||
return;
|
if (this.dirConfig == null) {
|
||||||
}
|
return;
|
||||||
|
|
||||||
this.dirConfig = await this.configurationService.getDirectory<GSuiteConfiguration>(DirectoryType.GSuite);
|
|
||||||
if (this.dirConfig == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.syncConfig = await this.configurationService.getSync();
|
|
||||||
if (this.syncConfig == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.auth();
|
|
||||||
|
|
||||||
let users: UserEntry[] = [];
|
|
||||||
if (this.syncConfig.users) {
|
|
||||||
users = await this.getUsers();
|
|
||||||
}
|
|
||||||
|
|
||||||
let groups: GroupEntry[];
|
|
||||||
if (this.syncConfig.groups) {
|
|
||||||
const setFilter = this.createCustomSet(this.syncConfig.groupFilter);
|
|
||||||
groups = await this.getGroups(setFilter, users);
|
|
||||||
users = this.filterUsersFromGroupsSet(users, groups, setFilter, this.syncConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
return [groups, users];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getUsers(): Promise<UserEntry[]> {
|
this.syncConfig = await this.stateService.getSync();
|
||||||
const entries: UserEntry[] = [];
|
if (this.syncConfig == null) {
|
||||||
const query = this.createDirectoryQuery(this.syncConfig.userFilter);
|
return;
|
||||||
let nextPageToken: string = null;
|
|
||||||
|
|
||||||
const filter = this.createCustomSet(this.syncConfig.userFilter);
|
|
||||||
while (true) {
|
|
||||||
this.logService.info('Querying users - nextPageToken:' + nextPageToken);
|
|
||||||
const p = Object.assign({ query: query, pageToken: nextPageToken }, this.authParams);
|
|
||||||
const res = await this.service.users.list(p);
|
|
||||||
if (res.status !== 200) {
|
|
||||||
throw new Error('User list API failed: ' + res.statusText);
|
|
||||||
}
|
|
||||||
|
|
||||||
nextPageToken = res.data.nextPageToken;
|
|
||||||
if (res.data.users != null) {
|
|
||||||
for (const user of res.data.users) {
|
|
||||||
if (this.filterOutResult(filter, user.primaryEmail)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const entry = this.buildUser(user, false);
|
|
||||||
if (entry != null) {
|
|
||||||
entries.push(entry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextPageToken == null) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
nextPageToken = null;
|
|
||||||
while (true) {
|
|
||||||
this.logService.info('Querying deleted users - nextPageToken:' + nextPageToken);
|
|
||||||
const p = Object.assign({ showDeleted: true, query: query, pageToken: nextPageToken }, this.authParams);
|
|
||||||
const delRes = await this.service.users.list(p);
|
|
||||||
if (delRes.status !== 200) {
|
|
||||||
throw new Error('Deleted user list API failed: ' + delRes.statusText);
|
|
||||||
}
|
|
||||||
|
|
||||||
nextPageToken = delRes.data.nextPageToken;
|
|
||||||
if (delRes.data.users != null) {
|
|
||||||
for (const user of delRes.data.users) {
|
|
||||||
if (this.filterOutResult(filter, user.primaryEmail)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const entry = this.buildUser(user, true);
|
|
||||||
if (entry != null) {
|
|
||||||
entries.push(entry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextPageToken == null) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return entries;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildUser(user: admin_directory_v1.Schema$User, deleted: boolean) {
|
await this.auth();
|
||||||
if ((user.emails == null || user.emails === '') && !deleted) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const entry = new UserEntry();
|
let users: UserEntry[] = [];
|
||||||
entry.referenceId = user.id;
|
if (this.syncConfig.users) {
|
||||||
entry.externalId = user.id;
|
users = await this.getUsers();
|
||||||
entry.email = user.primaryEmail != null ? user.primaryEmail.trim().toLowerCase() : null;
|
}
|
||||||
entry.disabled = user.suspended || false;
|
|
||||||
entry.deleted = deleted;
|
let groups: GroupEntry[];
|
||||||
|
if (this.syncConfig.groups) {
|
||||||
|
const setFilter = this.createCustomSet(this.syncConfig.groupFilter);
|
||||||
|
groups = await this.getGroups(setFilter, users);
|
||||||
|
users = this.filterUsersFromGroupsSet(users, groups, setFilter, this.syncConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [groups, users];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getUsers(): Promise<UserEntry[]> {
|
||||||
|
const entries: UserEntry[] = [];
|
||||||
|
const query = this.createDirectoryQuery(this.syncConfig.userFilter);
|
||||||
|
let nextPageToken: string = null;
|
||||||
|
|
||||||
|
const filter = this.createCustomSet(this.syncConfig.userFilter);
|
||||||
|
while (true) {
|
||||||
|
this.logService.info("Querying users - nextPageToken:" + nextPageToken);
|
||||||
|
const p = Object.assign({ query: query, pageToken: nextPageToken }, this.authParams);
|
||||||
|
const res = await this.service.users.list(p);
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error("User list API failed: " + res.statusText);
|
||||||
|
}
|
||||||
|
|
||||||
|
nextPageToken = res.data.nextPageToken;
|
||||||
|
if (res.data.users != null) {
|
||||||
|
for (const user of res.data.users) {
|
||||||
|
if (this.filterOutResult(filter, user.primaryEmail)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const entry = this.buildUser(user, false);
|
||||||
|
if (entry != null) {
|
||||||
|
entries.push(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextPageToken == null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nextPageToken = null;
|
||||||
|
while (true) {
|
||||||
|
this.logService.info("Querying deleted users - nextPageToken:" + nextPageToken);
|
||||||
|
const p = Object.assign(
|
||||||
|
{ showDeleted: true, query: query, pageToken: nextPageToken },
|
||||||
|
this.authParams
|
||||||
|
);
|
||||||
|
const delRes = await this.service.users.list(p);
|
||||||
|
if (delRes.status !== 200) {
|
||||||
|
throw new Error("Deleted user list API failed: " + delRes.statusText);
|
||||||
|
}
|
||||||
|
|
||||||
|
nextPageToken = delRes.data.nextPageToken;
|
||||||
|
if (delRes.data.users != null) {
|
||||||
|
for (const user of delRes.data.users) {
|
||||||
|
if (this.filterOutResult(filter, user.primaryEmail)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const entry = this.buildUser(user, true);
|
||||||
|
if (entry != null) {
|
||||||
|
entries.push(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextPageToken == null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildUser(user: admin_directory_v1.Schema$User, deleted: boolean) {
|
||||||
|
if ((user.emails == null || user.emails === "") && !deleted) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entry = new UserEntry();
|
||||||
|
entry.referenceId = user.id;
|
||||||
|
entry.externalId = user.id;
|
||||||
|
entry.email = user.primaryEmail != null ? user.primaryEmail.trim().toLowerCase() : null;
|
||||||
|
entry.disabled = user.suspended || false;
|
||||||
|
entry.deleted = deleted;
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getGroups(
|
||||||
|
setFilter: [boolean, Set<string>],
|
||||||
|
users: UserEntry[]
|
||||||
|
): Promise<GroupEntry[]> {
|
||||||
|
const entries: GroupEntry[] = [];
|
||||||
|
let nextPageToken: string = null;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
this.logService.info("Querying groups - nextPageToken:" + nextPageToken);
|
||||||
|
const p = Object.assign({ pageToken: nextPageToken }, this.authParams);
|
||||||
|
const res = await this.service.groups.list(p);
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error("Group list API failed: " + res.statusText);
|
||||||
|
}
|
||||||
|
|
||||||
|
nextPageToken = res.data.nextPageToken;
|
||||||
|
if (res.data.groups != null) {
|
||||||
|
for (const group of res.data.groups) {
|
||||||
|
if (!this.filterOutResult(setFilter, group.name)) {
|
||||||
|
const entry = await this.buildGroup(group, users);
|
||||||
|
entries.push(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextPageToken == null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async buildGroup(group: admin_directory_v1.Schema$Group, users: UserEntry[]) {
|
||||||
|
let nextPageToken: string = null;
|
||||||
|
|
||||||
|
const entry = new GroupEntry();
|
||||||
|
entry.referenceId = group.id;
|
||||||
|
entry.externalId = group.id;
|
||||||
|
entry.name = group.name;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
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;
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
nextPageToken = memRes.data.nextPageToken;
|
||||||
|
if (memRes.data.members != null) {
|
||||||
|
for (const member of memRes.data.members) {
|
||||||
|
if (member.type == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const type = member.type.toLowerCase();
|
||||||
|
if (type === "user") {
|
||||||
|
if (member.status == null || member.status.toLowerCase() !== "active") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
entry.userMemberExternalIds.add(member.id);
|
||||||
|
} else if (type === "group") {
|
||||||
|
entry.groupMemberReferenceIds.add(member.id);
|
||||||
|
} else if (type === "customer") {
|
||||||
|
for (const user of users) {
|
||||||
|
entry.userMemberExternalIds.add(user.externalId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextPageToken == null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getGroups(setFilter: [boolean, Set<string>], users: UserEntry[]): Promise<GroupEntry[]> {
|
return entry;
|
||||||
const entries: GroupEntry[] = [];
|
}
|
||||||
let nextPageToken: string = null;
|
|
||||||
|
|
||||||
while (true) {
|
private async auth() {
|
||||||
this.logService.info('Querying groups - nextPageToken:' + nextPageToken);
|
if (
|
||||||
const p = Object.assign({ pageToken: nextPageToken }, this.authParams);
|
this.dirConfig.clientEmail == null ||
|
||||||
const res = await this.service.groups.list(p);
|
this.dirConfig.privateKey == null ||
|
||||||
if (res.status !== 200) {
|
this.dirConfig.adminUser == null ||
|
||||||
throw new Error('Group list API failed: ' + res.statusText);
|
this.dirConfig.domain == null
|
||||||
}
|
) {
|
||||||
|
throw new Error(this.i18nService.t("dirConfigIncomplete"));
|
||||||
nextPageToken = res.data.nextPageToken;
|
|
||||||
if (res.data.groups != null) {
|
|
||||||
for (const group of res.data.groups) {
|
|
||||||
if (!this.filterOutResult(setFilter, group.name)) {
|
|
||||||
const entry = await this.buildGroup(group, users);
|
|
||||||
entries.push(entry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextPageToken == null) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return entries;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async buildGroup(group: admin_directory_v1.Schema$Group, users: UserEntry[]) {
|
this.client = new google.auth.JWT({
|
||||||
let nextPageToken: string = null;
|
email: this.dirConfig.clientEmail,
|
||||||
|
key: this.dirConfig.privateKey != null ? this.dirConfig.privateKey.trimLeft() : null,
|
||||||
|
subject: this.dirConfig.adminUser,
|
||||||
|
scopes: [
|
||||||
|
"https://www.googleapis.com/auth/admin.directory.user.readonly",
|
||||||
|
"https://www.googleapis.com/auth/admin.directory.group.readonly",
|
||||||
|
"https://www.googleapis.com/auth/admin.directory.group.member.readonly",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
const entry = new GroupEntry();
|
await this.client.authorize();
|
||||||
entry.referenceId = group.id;
|
|
||||||
entry.externalId = group.id;
|
|
||||||
entry.name = group.name;
|
|
||||||
|
|
||||||
while (true) {
|
this.authParams = {
|
||||||
const p = Object.assign({ groupKey: group.id, pageToken: nextPageToken }, this.authParams);
|
auth: this.client,
|
||||||
const memRes = await this.service.members.list(p);
|
};
|
||||||
if (memRes.status !== 200) {
|
if (this.dirConfig.domain != null && this.dirConfig.domain.trim() !== "") {
|
||||||
this.logService.warning('Group member list API failed: ' + memRes.statusText);
|
this.authParams.domain = this.dirConfig.domain;
|
||||||
return entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
nextPageToken = memRes.data.nextPageToken;
|
|
||||||
if (memRes.data.members != null) {
|
|
||||||
for (const member of memRes.data.members) {
|
|
||||||
if (member.type == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const type = member.type.toLowerCase();
|
|
||||||
if (type === 'user') {
|
|
||||||
if (member.status == null || member.status.toLowerCase() !== 'active') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
entry.userMemberExternalIds.add(member.id);
|
|
||||||
} else if (type === 'group') {
|
|
||||||
entry.groupMemberReferenceIds.add(member.id);
|
|
||||||
} else if (type === 'customer') {
|
|
||||||
for (const user of users) {
|
|
||||||
entry.userMemberExternalIds.add(user.externalId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextPageToken == null) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return entry;
|
|
||||||
}
|
}
|
||||||
|
if (this.dirConfig.customer != null && this.dirConfig.customer.trim() !== "") {
|
||||||
private async auth() {
|
this.authParams.customer = this.dirConfig.customer;
|
||||||
if (this.dirConfig.clientEmail == null || this.dirConfig.privateKey == null ||
|
|
||||||
this.dirConfig.adminUser == null || this.dirConfig.domain == null) {
|
|
||||||
throw new Error(this.i18nService.t('dirConfigIncomplete'));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.client = new google.auth.JWT({
|
|
||||||
email: this.dirConfig.clientEmail,
|
|
||||||
key: this.dirConfig.privateKey != null ? this.dirConfig.privateKey.trimLeft() : null,
|
|
||||||
subject: this.dirConfig.adminUser,
|
|
||||||
scopes: [
|
|
||||||
'https://www.googleapis.com/auth/admin.directory.user.readonly',
|
|
||||||
'https://www.googleapis.com/auth/admin.directory.group.readonly',
|
|
||||||
'https://www.googleapis.com/auth/admin.directory.group.member.readonly',
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.client.authorize();
|
|
||||||
|
|
||||||
this.authParams = {
|
|
||||||
auth: this.client,
|
|
||||||
};
|
|
||||||
if (this.dirConfig.domain != null && this.dirConfig.domain.trim() !== '') {
|
|
||||||
this.authParams.domain = this.dirConfig.domain;
|
|
||||||
}
|
|
||||||
if (this.dirConfig.customer != null && this.dirConfig.customer.trim() !== '') {
|
|
||||||
this.authParams.customer = this.dirConfig.customer;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
import * as fs from 'fs';
|
import * as fs from "fs";
|
||||||
import * as path from 'path';
|
import * as path from "path";
|
||||||
|
|
||||||
import { I18nService as BaseI18nService } from 'jslib-common/services/i18n.service';
|
import { I18nService as BaseI18nService } from "jslib-common/services/i18n.service";
|
||||||
|
|
||||||
export class I18nService extends BaseI18nService {
|
export class I18nService extends BaseI18nService {
|
||||||
constructor(systemLanguage: string, localesDirectory: string) {
|
constructor(systemLanguage: string, localesDirectory: string) {
|
||||||
super(systemLanguage, localesDirectory, (formattedLocale: string) => {
|
super(systemLanguage, localesDirectory, (formattedLocale: string) => {
|
||||||
const filePath = path.join(__dirname, this.localesDirectory + '/' + formattedLocale + '/messages.json');
|
const filePath = path.join(
|
||||||
const localesJson = fs.readFileSync(filePath, 'utf8');
|
__dirname,
|
||||||
const locales = JSON.parse(localesJson.replace(/^\uFEFF/, '')); // strip the BOM
|
this.localesDirectory + "/" + formattedLocale + "/messages.json"
|
||||||
return Promise.resolve(locales);
|
);
|
||||||
});
|
const localesJson = fs.readFileSync(filePath, "utf8");
|
||||||
}
|
const locales = JSON.parse(localesJson.replace(/^\uFEFF/, "")); // strip the BOM
|
||||||
|
return Promise.resolve(locales);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,25 @@
|
|||||||
import {
|
import { deletePassword, getPassword, setPassword } from "keytar";
|
||||||
deletePassword,
|
|
||||||
getPassword,
|
|
||||||
setPassword,
|
|
||||||
} from 'keytar';
|
|
||||||
|
|
||||||
import { StorageService } from 'jslib-common/abstractions/storage.service';
|
import { StorageService } from "jslib-common/abstractions/storage.service";
|
||||||
|
|
||||||
export class KeytarSecureStorageService implements StorageService {
|
export class KeytarSecureStorageService implements StorageService {
|
||||||
constructor(private serviceName: string) { }
|
constructor(private serviceName: string) {}
|
||||||
|
|
||||||
get<T>(key: string): Promise<T> {
|
get<T>(key: string): Promise<T> {
|
||||||
return getPassword(this.serviceName, key).then(val => {
|
return getPassword(this.serviceName, key).then((val) => {
|
||||||
return JSON.parse(val) as T;
|
return JSON.parse(val) as T;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async has(key: string): Promise<boolean> {
|
async has(key: string): Promise<boolean> {
|
||||||
return (await this.get(key)) != null;
|
return (await this.get(key)) != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
save(key: string, obj: any): Promise<any> {
|
save(key: string, obj: any): Promise<any> {
|
||||||
return setPassword(this.serviceName, key, JSON.stringify(obj));
|
return setPassword(this.serviceName, key, JSON.stringify(obj));
|
||||||
}
|
}
|
||||||
|
|
||||||
remove(key: string): Promise<any> {
|
remove(key: string): Promise<any> {
|
||||||
return deletePassword(this.serviceName, key);
|
return deletePassword(this.serviceName, key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,448 +1,504 @@
|
|||||||
import * as fs from 'fs';
|
import * as fs from "fs";
|
||||||
import * as ldap from 'ldapjs';
|
import * as ldap from "ldapjs";
|
||||||
|
|
||||||
import { checkServerIdentity, PeerCertificate } from 'tls';
|
import { checkServerIdentity, PeerCertificate } from "tls";
|
||||||
|
|
||||||
import { DirectoryType } from '../enums/directoryType';
|
import { DirectoryType } from "../enums/directoryType";
|
||||||
|
|
||||||
import { GroupEntry } from '../models/groupEntry';
|
import { GroupEntry } from "../models/groupEntry";
|
||||||
import { LdapConfiguration } from '../models/ldapConfiguration';
|
import { LdapConfiguration } from "../models/ldapConfiguration";
|
||||||
import { SyncConfiguration } from '../models/syncConfiguration';
|
import { SyncConfiguration } from "../models/syncConfiguration";
|
||||||
import { UserEntry } from '../models/userEntry';
|
import { UserEntry } from "../models/userEntry";
|
||||||
|
|
||||||
import { ConfigurationService } from './configuration.service';
|
import { IDirectoryService } from "./directory.service";
|
||||||
import { IDirectoryService } from './directory.service';
|
|
||||||
|
|
||||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
import { LogService } from 'jslib-common/abstractions/log.service';
|
import { LogService } from "jslib-common/abstractions/log.service";
|
||||||
|
import { StateService } from "../abstractions/state.service";
|
||||||
|
|
||||||
import { Utils } from 'jslib-common/misc/utils';
|
import { Utils } from "jslib-common/misc/utils";
|
||||||
|
|
||||||
const UserControlAccountDisabled = 2;
|
const UserControlAccountDisabled = 2;
|
||||||
|
|
||||||
export class LdapDirectoryService implements IDirectoryService {
|
export class LdapDirectoryService implements IDirectoryService {
|
||||||
private client: ldap.Client;
|
private client: ldap.Client;
|
||||||
private dirConfig: LdapConfiguration;
|
private dirConfig: LdapConfiguration;
|
||||||
private syncConfig: SyncConfiguration;
|
private syncConfig: SyncConfiguration;
|
||||||
|
|
||||||
constructor(private configurationService: ConfigurationService, private logService: LogService,
|
constructor(
|
||||||
private i18nService: I18nService) { }
|
private logService: LogService,
|
||||||
|
private i18nService: I18nService,
|
||||||
|
private stateService: StateService
|
||||||
|
) {}
|
||||||
|
|
||||||
async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> {
|
async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> {
|
||||||
const type = await this.configurationService.getDirectoryType();
|
const type = await this.stateService.getDirectoryType();
|
||||||
if (type !== DirectoryType.Ldap) {
|
if (type !== DirectoryType.Ldap) {
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
this.dirConfig = await this.configurationService.getDirectory<LdapConfiguration>(DirectoryType.Ldap);
|
|
||||||
if (this.dirConfig == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.syncConfig = await this.configurationService.getSync();
|
|
||||||
if (this.syncConfig == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.bind();
|
|
||||||
|
|
||||||
let users: UserEntry[];
|
|
||||||
if (this.syncConfig.users) {
|
|
||||||
users = await this.getUsers(force);
|
|
||||||
}
|
|
||||||
|
|
||||||
let groups: GroupEntry[];
|
|
||||||
if (this.syncConfig.groups) {
|
|
||||||
let groupForce = force;
|
|
||||||
if (!groupForce && users != null) {
|
|
||||||
const activeUsers = users.filter(u => !u.deleted && !u.disabled);
|
|
||||||
groupForce = activeUsers.length > 0;
|
|
||||||
}
|
|
||||||
groups = await this.getGroups(groupForce);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.unbind();
|
|
||||||
return [groups, users];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getUsers(force: boolean): Promise<UserEntry[]> {
|
this.dirConfig = await this.stateService.getDirectory<LdapConfiguration>(DirectoryType.Ldap);
|
||||||
const lastSync = await this.configurationService.getLastUserSyncDate();
|
if (this.dirConfig == null) {
|
||||||
let filter = this.buildBaseFilter(this.syncConfig.userObjectClass, this.syncConfig.userFilter);
|
return;
|
||||||
filter = this.buildRevisionFilter(filter, force, lastSync);
|
|
||||||
|
|
||||||
const path = this.makeSearchPath(this.syncConfig.userPath);
|
|
||||||
this.logService.info('User search: ' + path + ' => ' + filter);
|
|
||||||
|
|
||||||
const regularUsers = await this.search<UserEntry>(path, filter, (se: any) => this.buildUser(se, false));
|
|
||||||
if (!this.dirConfig.ad) {
|
|
||||||
return regularUsers;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
let deletedFilter = this.buildBaseFilter(this.syncConfig.userObjectClass, '(isDeleted=TRUE)');
|
|
||||||
deletedFilter = this.buildRevisionFilter(deletedFilter, force, lastSync);
|
|
||||||
|
|
||||||
const deletedPath = this.makeSearchPath('CN=Deleted Objects');
|
|
||||||
this.logService.info('Deleted user search: ' + deletedPath + ' => ' + deletedFilter);
|
|
||||||
|
|
||||||
const delControl = new (ldap as any).Control({ type: '1.2.840.113556.1.4.417', criticality: true });
|
|
||||||
const deletedUsers = await this.search<UserEntry>(deletedPath, deletedFilter,
|
|
||||||
(se: any) => this.buildUser(se, true), [delControl]);
|
|
||||||
return regularUsers.concat(deletedUsers);
|
|
||||||
} catch (e) {
|
|
||||||
this.logService.warning('Cannot query deleted users.');
|
|
||||||
return regularUsers;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildUser(searchEntry: any, deleted: boolean): UserEntry {
|
this.syncConfig = await this.stateService.getSync();
|
||||||
const user = new UserEntry();
|
if (this.syncConfig == null) {
|
||||||
user.referenceId = searchEntry.objectName;
|
return;
|
||||||
user.deleted = deleted;
|
|
||||||
|
|
||||||
if (user.referenceId == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
user.externalId = this.getExternalId(searchEntry, user.referenceId);
|
|
||||||
user.disabled = this.entryDisabled(searchEntry);
|
|
||||||
user.email = this.getAttr(searchEntry, this.syncConfig.userEmailAttribute);
|
|
||||||
if (user.email == null && this.syncConfig.useEmailPrefixSuffix &&
|
|
||||||
this.syncConfig.emailPrefixAttribute != null && this.syncConfig.emailSuffix != null) {
|
|
||||||
const prefixAttr = this.getAttr(searchEntry, this.syncConfig.emailPrefixAttribute);
|
|
||||||
if (prefixAttr != null) {
|
|
||||||
user.email = prefixAttr + this.syncConfig.emailSuffix;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.email != null) {
|
|
||||||
user.email = user.email.trim().toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user.deleted && (user.email == null || user.email.trim() === '')) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return user;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getGroups(force: boolean): Promise<GroupEntry[]> {
|
await this.bind();
|
||||||
const entries: GroupEntry[] = [];
|
|
||||||
|
|
||||||
const lastSync = await this.configurationService.getLastUserSyncDate();
|
let users: UserEntry[];
|
||||||
const originalFilter = this.buildBaseFilter(this.syncConfig.groupObjectClass, this.syncConfig.groupFilter);
|
if (this.syncConfig.users) {
|
||||||
let filter = originalFilter;
|
users = await this.getUsers(force);
|
||||||
const revisionFilter = this.buildRevisionFilter(filter, force, lastSync);
|
}
|
||||||
const searchSinceRevision = filter !== revisionFilter;
|
|
||||||
filter = revisionFilter;
|
|
||||||
|
|
||||||
const path = this.makeSearchPath(this.syncConfig.groupPath);
|
let groups: GroupEntry[];
|
||||||
this.logService.info('Group search: ' + path + ' => ' + filter);
|
if (this.syncConfig.groups) {
|
||||||
|
let groupForce = force;
|
||||||
|
if (!groupForce && users != null) {
|
||||||
|
const activeUsers = users.filter((u) => !u.deleted && !u.disabled);
|
||||||
|
groupForce = activeUsers.length > 0;
|
||||||
|
}
|
||||||
|
groups = await this.getGroups(groupForce);
|
||||||
|
}
|
||||||
|
|
||||||
let groupSearchEntries: any[] = [];
|
await this.unbind();
|
||||||
const initialSearchGroupIds = await this.search<string>(path, filter, (se: any) => {
|
return [groups, users];
|
||||||
groupSearchEntries.push(se);
|
}
|
||||||
return se.objectName;
|
|
||||||
|
private async getUsers(force: 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);
|
||||||
|
|
||||||
|
const path = this.makeSearchPath(this.syncConfig.userPath);
|
||||||
|
this.logService.info("User search: " + path + " => " + filter);
|
||||||
|
|
||||||
|
const regularUsers = await this.search<UserEntry>(path, filter, (se: any) =>
|
||||||
|
this.buildUser(se, false)
|
||||||
|
);
|
||||||
|
if (!this.dirConfig.ad) {
|
||||||
|
return regularUsers;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let deletedFilter = this.buildBaseFilter(this.syncConfig.userObjectClass, "(isDeleted=TRUE)");
|
||||||
|
deletedFilter = this.buildRevisionFilter(deletedFilter, force, lastSync);
|
||||||
|
|
||||||
|
const deletedPath = this.makeSearchPath("CN=Deleted Objects");
|
||||||
|
this.logService.info("Deleted user search: " + deletedPath + " => " + deletedFilter);
|
||||||
|
|
||||||
|
const delControl = new (ldap as any).Control({
|
||||||
|
type: "1.2.840.113556.1.4.417",
|
||||||
|
criticality: true,
|
||||||
|
});
|
||||||
|
const deletedUsers = await this.search<UserEntry>(
|
||||||
|
deletedPath,
|
||||||
|
deletedFilter,
|
||||||
|
(se: any) => this.buildUser(se, true),
|
||||||
|
[delControl]
|
||||||
|
);
|
||||||
|
return regularUsers.concat(deletedUsers);
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.warning("Cannot query deleted users.");
|
||||||
|
return regularUsers;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildUser(searchEntry: any, deleted: boolean): UserEntry {
|
||||||
|
const user = new UserEntry();
|
||||||
|
user.referenceId = searchEntry.objectName;
|
||||||
|
user.deleted = deleted;
|
||||||
|
|
||||||
|
if (user.referenceId == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
user.externalId = this.getExternalId(searchEntry, user.referenceId);
|
||||||
|
user.disabled = this.entryDisabled(searchEntry);
|
||||||
|
user.email = this.getAttr(searchEntry, this.syncConfig.userEmailAttribute);
|
||||||
|
if (
|
||||||
|
user.email == null &&
|
||||||
|
this.syncConfig.useEmailPrefixSuffix &&
|
||||||
|
this.syncConfig.emailPrefixAttribute != null &&
|
||||||
|
this.syncConfig.emailSuffix != null
|
||||||
|
) {
|
||||||
|
const prefixAttr = this.getAttr(searchEntry, this.syncConfig.emailPrefixAttribute);
|
||||||
|
if (prefixAttr != null) {
|
||||||
|
user.email = prefixAttr + this.syncConfig.emailSuffix;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.email != null) {
|
||||||
|
user.email = user.email.trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.deleted && (user.email == null || user.email.trim() === "")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getGroups(force: boolean): Promise<GroupEntry[]> {
|
||||||
|
const entries: GroupEntry[] = [];
|
||||||
|
|
||||||
|
const lastSync = await this.stateService.getLastUserSync();
|
||||||
|
const originalFilter = this.buildBaseFilter(
|
||||||
|
this.syncConfig.groupObjectClass,
|
||||||
|
this.syncConfig.groupFilter
|
||||||
|
);
|
||||||
|
let filter = originalFilter;
|
||||||
|
const revisionFilter = this.buildRevisionFilter(filter, force, lastSync);
|
||||||
|
const searchSinceRevision = filter !== revisionFilter;
|
||||||
|
filter = revisionFilter;
|
||||||
|
|
||||||
|
const path = this.makeSearchPath(this.syncConfig.groupPath);
|
||||||
|
this.logService.info("Group search: " + path + " => " + filter);
|
||||||
|
|
||||||
|
let groupSearchEntries: any[] = [];
|
||||||
|
const initialSearchGroupIds = await this.search<string>(path, filter, (se: any) => {
|
||||||
|
groupSearchEntries.push(se);
|
||||||
|
return se.objectName;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (searchSinceRevision && initialSearchGroupIds.length === 0) {
|
||||||
|
return [];
|
||||||
|
} else if (searchSinceRevision) {
|
||||||
|
groupSearchEntries = await this.search<string>(path, originalFilter, (se: any) => se);
|
||||||
|
}
|
||||||
|
|
||||||
|
const userFilter = this.buildBaseFilter(
|
||||||
|
this.syncConfig.userObjectClass,
|
||||||
|
this.syncConfig.userFilter
|
||||||
|
);
|
||||||
|
const userPath = this.makeSearchPath(this.syncConfig.userPath);
|
||||||
|
const userIdMap = new Map<string, string>();
|
||||||
|
await this.search<string>(userPath, userFilter, (se: any) => {
|
||||||
|
userIdMap.set(se.objectName, this.getExternalId(se, se.objectName));
|
||||||
|
return se;
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const se of groupSearchEntries) {
|
||||||
|
const group = this.buildGroup(se, userIdMap);
|
||||||
|
if (group != null) {
|
||||||
|
entries.push(group);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildGroup(searchEntry: any, userMap: Map<string, string>) {
|
||||||
|
const group = new GroupEntry();
|
||||||
|
group.referenceId = searchEntry.objectName;
|
||||||
|
if (group.referenceId == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
group.externalId = this.getExternalId(searchEntry, group.referenceId);
|
||||||
|
|
||||||
|
group.name = this.getAttr(searchEntry, this.syncConfig.groupNameAttribute);
|
||||||
|
if (group.name == null) {
|
||||||
|
group.name = this.getAttr(searchEntry, "cn");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (group.name == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const members = this.getAttrVals(searchEntry, this.syncConfig.memberAttribute);
|
||||||
|
if (members != null) {
|
||||||
|
for (const memDn of members) {
|
||||||
|
if (userMap.has(memDn) && !group.userMemberExternalIds.has(userMap.get(memDn))) {
|
||||||
|
group.userMemberExternalIds.add(userMap.get(memDn));
|
||||||
|
} else if (!group.groupMemberReferenceIds.has(memDn)) {
|
||||||
|
group.groupMemberReferenceIds.add(memDn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getExternalId(searchEntry: any, referenceId: string) {
|
||||||
|
const attrObj = this.getAttrObj(searchEntry, "objectGUID");
|
||||||
|
if (attrObj != null && attrObj._vals != null && attrObj._vals.length > 0) {
|
||||||
|
return this.bufToGuid(attrObj._vals[0]);
|
||||||
|
} else {
|
||||||
|
return referenceId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildBaseFilter(objectClass: string, subFilter: string): string {
|
||||||
|
let filter = this.buildObjectClassFilter(objectClass);
|
||||||
|
if (subFilter != null && subFilter.trim() !== "") {
|
||||||
|
filter = "(&" + filter + subFilter + ")";
|
||||||
|
}
|
||||||
|
return filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildObjectClassFilter(objectClass: string): string {
|
||||||
|
return "(&(objectClass=" + objectClass + "))";
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildRevisionFilter(baseFilter: string, force: boolean, lastRevisionDate: Date) {
|
||||||
|
const revisionAttr = this.syncConfig.revisionDateAttribute;
|
||||||
|
if (!force && lastRevisionDate != null && revisionAttr != null && revisionAttr.trim() !== "") {
|
||||||
|
const dateString = lastRevisionDate.toISOString().replace(/[-:T]/g, "").substr(0, 16) + "Z";
|
||||||
|
baseFilter = "(&" + baseFilter + "(" + revisionAttr + ">=" + dateString + "))";
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseFilter;
|
||||||
|
}
|
||||||
|
|
||||||
|
private makeSearchPath(pathPrefix: string) {
|
||||||
|
if (this.dirConfig.rootPath.toLowerCase().indexOf("dc=") === -1) {
|
||||||
|
return pathPrefix;
|
||||||
|
}
|
||||||
|
if (this.dirConfig.rootPath != null && this.dirConfig.rootPath.trim() !== "") {
|
||||||
|
const trimmedRootPath = this.dirConfig.rootPath.trim().toLowerCase();
|
||||||
|
let path = trimmedRootPath.substr(trimmedRootPath.indexOf("dc="));
|
||||||
|
if (pathPrefix != null && pathPrefix.trim() !== "") {
|
||||||
|
path = pathPrefix.trim() + "," + path;
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getAttrObj(searchEntry: any, attr: string): any {
|
||||||
|
if (searchEntry == null || searchEntry.attributes == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const attrs = searchEntry.attributes.filter((a: any) => a.type === attr);
|
||||||
|
if (
|
||||||
|
attrs == null ||
|
||||||
|
attrs.length === 0 ||
|
||||||
|
attrs[0].vals == null ||
|
||||||
|
attrs[0].vals.length === 0
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return attrs[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
private getAttrVals(searchEntry: any, attr: string): string[] {
|
||||||
|
const obj = this.getAttrObj(searchEntry, attr);
|
||||||
|
if (obj == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return obj.vals;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getAttr(searchEntry: any, attr: string): string {
|
||||||
|
const vals = this.getAttrVals(searchEntry, attr);
|
||||||
|
if (vals == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return vals[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
private entryDisabled(searchEntry: any): boolean {
|
||||||
|
const c = this.getAttr(searchEntry, "userAccountControl");
|
||||||
|
if (c != null) {
|
||||||
|
try {
|
||||||
|
const control = parseInt(c, null);
|
||||||
|
// tslint:disable-next-line
|
||||||
|
return (control & UserControlAccountDisabled) === UserControlAccountDisabled;
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async search<T>(
|
||||||
|
path: string,
|
||||||
|
filter: string,
|
||||||
|
processEntry: (searchEntry: any) => T,
|
||||||
|
controls: ldap.Control[] = []
|
||||||
|
): Promise<T[]> {
|
||||||
|
const options: ldap.SearchOptions = {
|
||||||
|
filter: filter,
|
||||||
|
scope: "sub",
|
||||||
|
paged: this.dirConfig.pagedSearch,
|
||||||
|
};
|
||||||
|
const entries: T[] = [];
|
||||||
|
return new Promise<T[]>((resolve, reject) => {
|
||||||
|
this.client.search(path, options, controls, (err, res) => {
|
||||||
|
if (err != null) {
|
||||||
|
reject(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.on("error", (resErr) => {
|
||||||
|
reject(resErr);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (searchSinceRevision && initialSearchGroupIds.length === 0) {
|
res.on("searchEntry", (entry) => {
|
||||||
return [];
|
const e = processEntry(entry);
|
||||||
} else if (searchSinceRevision) {
|
if (e != null) {
|
||||||
groupSearchEntries = await this.search<string>(path, originalFilter, (se: any) => se);
|
entries.push(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
const userFilter = this.buildBaseFilter(this.syncConfig.userObjectClass, this.syncConfig.userFilter);
|
|
||||||
const userPath = this.makeSearchPath(this.syncConfig.userPath);
|
|
||||||
const userIdMap = new Map<string, string>();
|
|
||||||
await this.search<string>(userPath, userFilter, (se: any) => {
|
|
||||||
userIdMap.set(se.objectName, this.getExternalId(se, se.objectName));
|
|
||||||
return se;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const se of groupSearchEntries) {
|
res.on("end", (result) => {
|
||||||
const group = this.buildGroup(se, userIdMap);
|
resolve(entries);
|
||||||
if (group != null) {
|
});
|
||||||
entries.push(group);
|
});
|
||||||
}
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async bind(): Promise<any> {
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
if (this.dirConfig.hostname == null || this.dirConfig.port == null) {
|
||||||
|
reject(this.i18nService.t("dirConfigIncomplete"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const protocol = "ldap" + (this.dirConfig.ssl && !this.dirConfig.startTls ? "s" : "");
|
||||||
|
const url = protocol + "://" + this.dirConfig.hostname + ":" + this.dirConfig.port;
|
||||||
|
const options: ldap.ClientOptions = {
|
||||||
|
url: url.trim().toLowerCase(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const tlsOptions: any = {};
|
||||||
|
if (this.dirConfig.ssl) {
|
||||||
|
if (this.dirConfig.sslAllowUnauthorized) {
|
||||||
|
tlsOptions.rejectUnauthorized = !this.dirConfig.sslAllowUnauthorized;
|
||||||
}
|
}
|
||||||
|
if (!this.dirConfig.startTls) {
|
||||||
return entries;
|
if (
|
||||||
}
|
this.dirConfig.sslCaPath != null &&
|
||||||
|
this.dirConfig.sslCaPath !== "" &&
|
||||||
private buildGroup(searchEntry: any, userMap: Map<string, string>) {
|
fs.existsSync(this.dirConfig.sslCaPath)
|
||||||
const group = new GroupEntry();
|
) {
|
||||||
group.referenceId = searchEntry.objectName;
|
tlsOptions.ca = [fs.readFileSync(this.dirConfig.sslCaPath)];
|
||||||
if (group.referenceId == null) {
|
}
|
||||||
return null;
|
if (
|
||||||
}
|
this.dirConfig.sslCertPath != null &&
|
||||||
|
this.dirConfig.sslCertPath !== "" &&
|
||||||
group.externalId = this.getExternalId(searchEntry, group.referenceId);
|
fs.existsSync(this.dirConfig.sslCertPath)
|
||||||
|
) {
|
||||||
group.name = this.getAttr(searchEntry, this.syncConfig.groupNameAttribute);
|
tlsOptions.cert = fs.readFileSync(this.dirConfig.sslCertPath);
|
||||||
if (group.name == null) {
|
}
|
||||||
group.name = this.getAttr(searchEntry, 'cn');
|
if (
|
||||||
}
|
this.dirConfig.sslKeyPath != null &&
|
||||||
|
this.dirConfig.sslKeyPath !== "" &&
|
||||||
if (group.name == null) {
|
fs.existsSync(this.dirConfig.sslKeyPath)
|
||||||
return null;
|
) {
|
||||||
}
|
tlsOptions.key = fs.readFileSync(this.dirConfig.sslKeyPath);
|
||||||
|
}
|
||||||
const members = this.getAttrVals(searchEntry, this.syncConfig.memberAttribute);
|
|
||||||
if (members != null) {
|
|
||||||
for (const memDn of members) {
|
|
||||||
if (userMap.has(memDn) && !group.userMemberExternalIds.has(userMap.get(memDn))) {
|
|
||||||
group.userMemberExternalIds.add(userMap.get(memDn));
|
|
||||||
} else if (!group.groupMemberReferenceIds.has(memDn)) {
|
|
||||||
group.groupMemberReferenceIds.add(memDn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return group;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getExternalId(searchEntry: any, referenceId: string) {
|
|
||||||
const attrObj = this.getAttrObj(searchEntry, 'objectGUID');
|
|
||||||
if (attrObj != null && attrObj._vals != null && attrObj._vals.length > 0) {
|
|
||||||
return this.bufToGuid(attrObj._vals[0]);
|
|
||||||
} else {
|
} else {
|
||||||
return referenceId;
|
if (
|
||||||
|
this.dirConfig.tlsCaPath != null &&
|
||||||
|
this.dirConfig.tlsCaPath !== "" &&
|
||||||
|
fs.existsSync(this.dirConfig.tlsCaPath)
|
||||||
|
) {
|
||||||
|
tlsOptions.ca = [fs.readFileSync(this.dirConfig.tlsCaPath)];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildBaseFilter(objectClass: string, subFilter: string): string {
|
tlsOptions.checkServerIdentity = this.checkServerIdentityAltNames;
|
||||||
let filter = this.buildObjectClassFilter(objectClass);
|
options.tlsOptions = tlsOptions;
|
||||||
if (subFilter != null && subFilter.trim() !== '') {
|
|
||||||
filter = '(&' + filter + subFilter + ')';
|
|
||||||
}
|
|
||||||
return filter;
|
|
||||||
}
|
|
||||||
|
|
||||||
private buildObjectClassFilter(objectClass: string): string {
|
this.client = ldap.createClient(options);
|
||||||
return '(&(objectClass=' + objectClass + '))';
|
|
||||||
}
|
|
||||||
|
|
||||||
private buildRevisionFilter(baseFilter: string, force: boolean, lastRevisionDate: Date) {
|
const user =
|
||||||
const revisionAttr = this.syncConfig.revisionDateAttribute;
|
this.dirConfig.username == null || this.dirConfig.username.trim() === ""
|
||||||
if (!force && lastRevisionDate != null && revisionAttr != null && revisionAttr.trim() !== '') {
|
? null
|
||||||
const dateString = lastRevisionDate.toISOString().replace(/[-:T]/g, '').substr(0, 16) + 'Z';
|
: this.dirConfig.username;
|
||||||
baseFilter = '(&' + baseFilter + '(' + revisionAttr + '>=' + dateString + '))';
|
const pass =
|
||||||
}
|
this.dirConfig.password == null || this.dirConfig.password.trim() === ""
|
||||||
|
? null
|
||||||
|
: this.dirConfig.password;
|
||||||
|
|
||||||
return baseFilter;
|
if (user == null || pass == null) {
|
||||||
}
|
reject(this.i18nService.t("usernamePasswordNotConfigured"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
private makeSearchPath(pathPrefix: string) {
|
if (this.dirConfig.startTls && this.dirConfig.ssl) {
|
||||||
if (this.dirConfig.rootPath.toLowerCase().indexOf('dc=') === -1) {
|
this.client.starttls(options.tlsOptions, undefined, (err, res) => {
|
||||||
return pathPrefix;
|
if (err != null) {
|
||||||
}
|
reject(err.message);
|
||||||
if (this.dirConfig.rootPath != null && this.dirConfig.rootPath.trim() !== '') {
|
} else {
|
||||||
const trimmedRootPath = this.dirConfig.rootPath.trim().toLowerCase();
|
this.client.bind(user, pass, (err2) => {
|
||||||
let path = trimmedRootPath.substr(trimmedRootPath.indexOf('dc='));
|
if (err2 != null) {
|
||||||
if (pathPrefix != null && pathPrefix.trim() !== '') {
|
reject(err2.message);
|
||||||
path = pathPrefix.trim() + ',' + path;
|
} else {
|
||||||
}
|
resolve();
|
||||||
return path;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getAttrObj(searchEntry: any, attr: string): any {
|
|
||||||
if (searchEntry == null || searchEntry.attributes == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const attrs = searchEntry.attributes.filter((a: any) => a.type === attr);
|
|
||||||
if (attrs == null || attrs.length === 0 || attrs[0].vals == null || attrs[0].vals.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return attrs[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
private getAttrVals(searchEntry: any, attr: string): string[] {
|
|
||||||
const obj = this.getAttrObj(searchEntry, attr);
|
|
||||||
if (obj == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return obj.vals;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getAttr(searchEntry: any, attr: string): string {
|
|
||||||
const vals = this.getAttrVals(searchEntry, attr);
|
|
||||||
if (vals == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return vals[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
private entryDisabled(searchEntry: any): boolean {
|
|
||||||
const c = this.getAttr(searchEntry, 'userAccountControl');
|
|
||||||
if (c != null) {
|
|
||||||
try {
|
|
||||||
const control = parseInt(c, null);
|
|
||||||
// tslint:disable-next-line
|
|
||||||
return (control & UserControlAccountDisabled) === UserControlAccountDisabled;
|
|
||||||
} catch { }
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async search<T>(path: string, filter: string, processEntry: (searchEntry: any) => T,
|
|
||||||
controls: ldap.Control[] = []): Promise<T[]> {
|
|
||||||
const options: ldap.SearchOptions = {
|
|
||||||
filter: filter,
|
|
||||||
scope: 'sub',
|
|
||||||
paged: this.dirConfig.pagedSearch,
|
|
||||||
};
|
|
||||||
const entries: T[] = [];
|
|
||||||
return new Promise<T[]>((resolve, reject) => {
|
|
||||||
this.client.search(path, options, controls, (err, res) => {
|
|
||||||
if (err != null) {
|
|
||||||
reject(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.on('error', resErr => {
|
|
||||||
reject(resErr);
|
|
||||||
});
|
|
||||||
|
|
||||||
res.on('searchEntry', entry => {
|
|
||||||
const e = processEntry(entry);
|
|
||||||
if (e != null) {
|
|
||||||
entries.push(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
res.on('end', result => {
|
|
||||||
resolve(entries);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
} else {
|
||||||
|
this.client.bind(user, pass, (err) => {
|
||||||
private async bind(): Promise<any> {
|
if (err != null) {
|
||||||
return new Promise<void>((resolve, reject) => {
|
reject(err.message);
|
||||||
if (this.dirConfig.hostname == null || this.dirConfig.port == null) {
|
} else {
|
||||||
reject(this.i18nService.t('dirConfigIncomplete'));
|
resolve();
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
const protocol = 'ldap' + (this.dirConfig.ssl && !this.dirConfig.startTls ? 's' : '');
|
|
||||||
const url = protocol + '://' + this.dirConfig.hostname +
|
|
||||||
':' + this.dirConfig.port;
|
|
||||||
const options: ldap.ClientOptions = {
|
|
||||||
url: url.trim().toLowerCase(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const tlsOptions: any = {};
|
|
||||||
if (this.dirConfig.ssl) {
|
|
||||||
if (this.dirConfig.sslAllowUnauthorized) {
|
|
||||||
tlsOptions.rejectUnauthorized = !this.dirConfig.sslAllowUnauthorized;
|
|
||||||
}
|
|
||||||
if (!this.dirConfig.startTls) {
|
|
||||||
if (this.dirConfig.sslCaPath != null && this.dirConfig.sslCaPath !== '' &&
|
|
||||||
fs.existsSync(this.dirConfig.sslCaPath)) {
|
|
||||||
tlsOptions.ca = [fs.readFileSync(this.dirConfig.sslCaPath)];
|
|
||||||
}
|
|
||||||
if (this.dirConfig.sslCertPath != null && this.dirConfig.sslCertPath !== '' &&
|
|
||||||
fs.existsSync(this.dirConfig.sslCertPath)) {
|
|
||||||
tlsOptions.cert = fs.readFileSync(this.dirConfig.sslCertPath);
|
|
||||||
}
|
|
||||||
if (this.dirConfig.sslKeyPath != null && this.dirConfig.sslKeyPath !== '' &&
|
|
||||||
fs.existsSync(this.dirConfig.sslKeyPath)) {
|
|
||||||
tlsOptions.key = fs.readFileSync(this.dirConfig.sslKeyPath);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (this.dirConfig.tlsCaPath != null && this.dirConfig.tlsCaPath !== '' &&
|
|
||||||
fs.existsSync(this.dirConfig.tlsCaPath)) {
|
|
||||||
tlsOptions.ca = [fs.readFileSync(this.dirConfig.tlsCaPath)];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tlsOptions.checkServerIdentity = this.checkServerIdentityAltNames;
|
|
||||||
options.tlsOptions = tlsOptions;
|
|
||||||
|
|
||||||
this.client = ldap.createClient(options);
|
|
||||||
|
|
||||||
const user = this.dirConfig.username == null || this.dirConfig.username.trim() === '' ? null :
|
|
||||||
this.dirConfig.username;
|
|
||||||
const pass = this.dirConfig.password == null || this.dirConfig.password.trim() === '' ? null :
|
|
||||||
this.dirConfig.password;
|
|
||||||
|
|
||||||
if (user == null || pass == null) {
|
|
||||||
reject(this.i18nService.t('usernamePasswordNotConfigured'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.dirConfig.startTls && this.dirConfig.ssl) {
|
|
||||||
this.client.starttls(options.tlsOptions, undefined, (err, res) => {
|
|
||||||
if (err != null) {
|
|
||||||
reject(err.message);
|
|
||||||
} else {
|
|
||||||
this.client.bind(user, pass, err2 => {
|
|
||||||
if (err2 != null) {
|
|
||||||
reject(err2.message);
|
|
||||||
} else {
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.client.bind(user, pass, err => {
|
|
||||||
if (err != null) {
|
|
||||||
reject(err.message);
|
|
||||||
} else {
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private async unbind(): Promise<void> {
|
private async unbind(): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.client.unbind(err => {
|
this.client.unbind((err) => {
|
||||||
if (err != null) {
|
if (err != null) {
|
||||||
reject(err);
|
reject(err);
|
||||||
} else {
|
} else {
|
||||||
resolve();
|
resolve();
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private bufToGuid(buf: Buffer) {
|
|
||||||
const arr = new Uint8Array(buf);
|
|
||||||
const p1 = arr.slice(0, 4).reverse().buffer;
|
|
||||||
const p2 = arr.slice(4, 6).reverse().buffer;
|
|
||||||
const p3 = arr.slice(6, 8).reverse().buffer;
|
|
||||||
const p4 = arr.slice(8, 10).buffer;
|
|
||||||
const p5 = arr.slice(10).buffer;
|
|
||||||
const guid = Utils.fromBufferToHex(p1) + '-' + Utils.fromBufferToHex(p2) + '-' + Utils.fromBufferToHex(p3) +
|
|
||||||
'-' + Utils.fromBufferToHex(p4) + '-' + Utils.fromBufferToHex(p5);
|
|
||||||
return guid.toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
private checkServerIdentityAltNames(host: string, cert: PeerCertificate) {
|
|
||||||
// Fixes the cert representation when subject is empty and altNames are present
|
|
||||||
// Required for node versions < 12.14.1 (which could be used for bwdc cli)
|
|
||||||
// Adapted from: https://github.com/auth0/ad-ldap-connector/commit/1f4dd2be6ed93dda591dd31ed5483a9b452a8d2a
|
|
||||||
// See https://github.com/nodejs/node/issues/11771 for details
|
|
||||||
if (cert && cert.subject == null && /(IP|DNS|URL)/.test(cert.subjectaltname)) {
|
|
||||||
cert.subject = {
|
|
||||||
C: null,
|
|
||||||
ST: null,
|
|
||||||
L: null,
|
|
||||||
O: null,
|
|
||||||
OU: null,
|
|
||||||
CN: null,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return checkServerIdentity(host, cert);
|
private bufToGuid(buf: Buffer) {
|
||||||
|
const arr = new Uint8Array(buf);
|
||||||
|
const p1 = arr.slice(0, 4).reverse().buffer;
|
||||||
|
const p2 = arr.slice(4, 6).reverse().buffer;
|
||||||
|
const p3 = arr.slice(6, 8).reverse().buffer;
|
||||||
|
const p4 = arr.slice(8, 10).buffer;
|
||||||
|
const p5 = arr.slice(10).buffer;
|
||||||
|
const guid =
|
||||||
|
Utils.fromBufferToHex(p1) +
|
||||||
|
"-" +
|
||||||
|
Utils.fromBufferToHex(p2) +
|
||||||
|
"-" +
|
||||||
|
Utils.fromBufferToHex(p3) +
|
||||||
|
"-" +
|
||||||
|
Utils.fromBufferToHex(p4) +
|
||||||
|
"-" +
|
||||||
|
Utils.fromBufferToHex(p5);
|
||||||
|
return guid.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
private checkServerIdentityAltNames(host: string, cert: PeerCertificate) {
|
||||||
|
// Fixes the cert representation when subject is empty and altNames are present
|
||||||
|
// Required for node versions < 12.14.1 (which could be used for bwdc cli)
|
||||||
|
// Adapted from: https://github.com/auth0/ad-ldap-connector/commit/1f4dd2be6ed93dda591dd31ed5483a9b452a8d2a
|
||||||
|
// See https://github.com/nodejs/node/issues/11771 for details
|
||||||
|
if (cert && cert.subject == null && /(IP|DNS|URL)/.test(cert.subjectaltname)) {
|
||||||
|
cert.subject = {
|
||||||
|
C: null,
|
||||||
|
ST: null,
|
||||||
|
L: null,
|
||||||
|
O: null,
|
||||||
|
OU: null,
|
||||||
|
CN: null,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return checkServerIdentity(host, cert);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,34 @@
|
|||||||
import * as lock from 'proper-lockfile';
|
import * as lock from "proper-lockfile";
|
||||||
|
|
||||||
import { LogService } from 'jslib-common/abstractions/log.service';
|
import { LogService } from "jslib-common/abstractions/log.service";
|
||||||
|
|
||||||
import { LowdbStorageService as LowdbStorageServiceBase } from 'jslib-node/services/lowdbStorage.service';
|
import { LowdbStorageService as LowdbStorageServiceBase } from "jslib-node/services/lowdbStorage.service";
|
||||||
|
|
||||||
import { Utils } from 'jslib-common/misc/utils';
|
import { Utils } from "jslib-common/misc/utils";
|
||||||
|
|
||||||
export class LowdbStorageService extends LowdbStorageServiceBase {
|
export class LowdbStorageService extends LowdbStorageServiceBase {
|
||||||
constructor(logService: LogService, defaults?: any, dir?: string, allowCache = false, private requireLock = false) {
|
constructor(
|
||||||
super(logService, defaults, dir, allowCache);
|
logService: LogService,
|
||||||
}
|
defaults?: any,
|
||||||
|
dir?: string,
|
||||||
|
allowCache = false,
|
||||||
|
private requireLock = false
|
||||||
|
) {
|
||||||
|
super(logService, defaults, dir, allowCache);
|
||||||
|
}
|
||||||
|
|
||||||
protected async lockDbFile<T>(action: () => T): Promise<T> {
|
protected async lockDbFile<T>(action: () => T): Promise<T> {
|
||||||
if (this.requireLock && !Utils.isNullOrWhitespace(this.dataFilePath)) {
|
if (this.requireLock && !Utils.isNullOrWhitespace(this.dataFilePath)) {
|
||||||
this.logService.info('acquiring db file lock');
|
this.logService.info("acquiring db file lock");
|
||||||
return await lock.lock(this.dataFilePath, { retries: 3 }).then(release => {
|
return await lock.lock(this.dataFilePath, { retries: 3 }).then((release) => {
|
||||||
try {
|
try {
|
||||||
return action();
|
return action();
|
||||||
} finally {
|
} finally {
|
||||||
release();
|
release();
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return action();
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return action();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
import { EnvironmentService } from 'jslib-common/abstractions/environment.service';
|
|
||||||
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
|
|
||||||
import { TokenService } from 'jslib-common/abstractions/token.service';
|
|
||||||
|
|
||||||
import { NodeApiService as NodeApiServiceBase } from 'jslib-node/services/nodeApi.service';
|
|
||||||
|
|
||||||
export class NodeApiService extends NodeApiServiceBase {
|
|
||||||
constructor(tokenService: TokenService, platformUtilsService: PlatformUtilsService, environmentService: EnvironmentService,
|
|
||||||
private refreshTokenCallback: () => Promise<void>, logoutCallback: (expired: boolean) => Promise<void>,
|
|
||||||
customUserAgent: string = null, apiKeyRefresh: (clientId: string, clientSecret: string) => Promise<any>) {
|
|
||||||
super(tokenService, platformUtilsService, environmentService, logoutCallback, customUserAgent, apiKeyRefresh);
|
|
||||||
}
|
|
||||||
|
|
||||||
doRefreshToken(): Promise<void> {
|
|
||||||
return this.refreshTokenCallback();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,253 +1,269 @@
|
|||||||
import { DirectoryType } from '../enums/directoryType';
|
import { DirectoryType } from "../enums/directoryType";
|
||||||
|
|
||||||
import { GroupEntry } from '../models/groupEntry';
|
import { GroupEntry } from "../models/groupEntry";
|
||||||
import { OktaConfiguration } from '../models/oktaConfiguration';
|
import { OktaConfiguration } from "../models/oktaConfiguration";
|
||||||
import { SyncConfiguration } from '../models/syncConfiguration';
|
import { SyncConfiguration } from "../models/syncConfiguration";
|
||||||
import { UserEntry } from '../models/userEntry';
|
import { UserEntry } from "../models/userEntry";
|
||||||
|
|
||||||
import { BaseDirectoryService } from './baseDirectory.service';
|
import { BaseDirectoryService } from "./baseDirectory.service";
|
||||||
import { ConfigurationService } from './configuration.service';
|
import { IDirectoryService } from "./directory.service";
|
||||||
import { IDirectoryService } from './directory.service';
|
|
||||||
|
|
||||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
import { LogService } from 'jslib-common/abstractions/log.service';
|
import { LogService } from "jslib-common/abstractions/log.service";
|
||||||
|
|
||||||
import * as https from 'https';
|
import * as https from "https";
|
||||||
|
import { StateService } from "../abstractions/state.service";
|
||||||
|
|
||||||
const DelayBetweenBuildGroupCallsInMilliseconds = 500;
|
const DelayBetweenBuildGroupCallsInMilliseconds = 500;
|
||||||
|
|
||||||
export class OktaDirectoryService extends BaseDirectoryService implements IDirectoryService {
|
export class OktaDirectoryService extends BaseDirectoryService implements IDirectoryService {
|
||||||
private dirConfig: OktaConfiguration;
|
private dirConfig: OktaConfiguration;
|
||||||
private syncConfig: SyncConfiguration;
|
private syncConfig: SyncConfiguration;
|
||||||
private lastBuildGroupCall: number;
|
private lastBuildGroupCall: number;
|
||||||
|
|
||||||
constructor(private configurationService: ConfigurationService, private logService: LogService,
|
constructor(
|
||||||
private i18nService: I18nService) {
|
private logService: LogService,
|
||||||
super();
|
private i18nService: I18nService,
|
||||||
|
private stateService: StateService
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> {
|
||||||
|
const type = await this.stateService.getDirectoryType();
|
||||||
|
if (type !== DirectoryType.Okta) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> {
|
this.dirConfig = await this.stateService.getDirectory<OktaConfiguration>(DirectoryType.Okta);
|
||||||
const type = await this.configurationService.getDirectoryType();
|
if (this.dirConfig == null) {
|
||||||
if (type !== DirectoryType.Okta) {
|
return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.dirConfig = await this.configurationService.getDirectory<OktaConfiguration>(DirectoryType.Okta);
|
|
||||||
if (this.dirConfig == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.syncConfig = await this.configurationService.getSync();
|
|
||||||
if (this.syncConfig == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.dirConfig.orgUrl == null || this.dirConfig.token == null) {
|
|
||||||
throw new Error(this.i18nService.t('dirConfigIncomplete'));
|
|
||||||
}
|
|
||||||
|
|
||||||
let users: UserEntry[];
|
|
||||||
if (this.syncConfig.users) {
|
|
||||||
users = await this.getUsers(force);
|
|
||||||
}
|
|
||||||
|
|
||||||
let groups: GroupEntry[];
|
|
||||||
if (this.syncConfig.groups) {
|
|
||||||
const setFilter = this.createCustomSet(this.syncConfig.groupFilter);
|
|
||||||
groups = await this.getGroups(this.forceGroup(force, users), setFilter);
|
|
||||||
users = this.filterUsersFromGroupsSet(users, groups, setFilter, this.syncConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
return [groups, users];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getUsers(force: boolean): Promise<UserEntry[]> {
|
this.syncConfig = await this.stateService.getSync();
|
||||||
const entries: UserEntry[] = [];
|
if (this.syncConfig == null) {
|
||||||
const lastSync = await this.configurationService.getLastUserSyncDate();
|
return;
|
||||||
const oktaFilter = this.buildOktaFilter(this.syncConfig.userFilter, force, lastSync);
|
}
|
||||||
const setFilter = this.createCustomSet(this.syncConfig.userFilter);
|
|
||||||
|
|
||||||
this.logService.info('Querying users.');
|
if (this.dirConfig.orgUrl == null || this.dirConfig.token == null) {
|
||||||
const usersPromise = this.apiGetMany('users?filter=' + this.encodeUrlParameter(oktaFilter))
|
throw new Error(this.i18nService.t("dirConfigIncomplete"));
|
||||||
.then((users: any[]) => {
|
}
|
||||||
for (const user of users) {
|
|
||||||
const entry = this.buildUser(user);
|
let users: UserEntry[];
|
||||||
if (entry != null && !this.filterOutResult(setFilter, entry.email)) {
|
if (this.syncConfig.users) {
|
||||||
entries.push(entry);
|
users = await this.getUsers(force);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let groups: GroupEntry[];
|
||||||
|
if (this.syncConfig.groups) {
|
||||||
|
const setFilter = this.createCustomSet(this.syncConfig.groupFilter);
|
||||||
|
groups = await this.getGroups(this.forceGroup(force, users), setFilter);
|
||||||
|
users = this.filterUsersFromGroupsSet(users, groups, setFilter, this.syncConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [groups, users];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getUsers(force: boolean): Promise<UserEntry[]> {
|
||||||
|
const entries: UserEntry[] = [];
|
||||||
|
const lastSync = await this.stateService.getLastUserSync();
|
||||||
|
const oktaFilter = this.buildOktaFilter(this.syncConfig.userFilter, force, lastSync);
|
||||||
|
const setFilter = this.createCustomSet(this.syncConfig.userFilter);
|
||||||
|
|
||||||
|
this.logService.info("Querying users.");
|
||||||
|
const usersPromise = this.apiGetMany(
|
||||||
|
"users?filter=" + this.encodeUrlParameter(oktaFilter)
|
||||||
|
).then((users: any[]) => {
|
||||||
|
for (const user of users) {
|
||||||
|
const entry = this.buildUser(user);
|
||||||
|
if (entry != null && !this.filterOutResult(setFilter, entry.email)) {
|
||||||
|
entries.push(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Deactivated users have to be queried for separately, only when no filter is provided in the first query
|
||||||
|
let deactUsersPromise: any;
|
||||||
|
if (oktaFilter == null || oktaFilter.indexOf("lastUpdated ") === -1) {
|
||||||
|
let deactOktaFilter = 'status eq "DEPROVISIONED"';
|
||||||
|
if (oktaFilter != null) {
|
||||||
|
deactOktaFilter = "(" + oktaFilter + ") and " + deactOktaFilter;
|
||||||
|
}
|
||||||
|
deactUsersPromise = this.apiGetMany(
|
||||||
|
"users?filter=" + this.encodeUrlParameter(deactOktaFilter)
|
||||||
|
).then((users: any[]) => {
|
||||||
|
for (const user of users) {
|
||||||
|
const entry = this.buildUser(user);
|
||||||
|
if (entry != null && !this.filterOutResult(setFilter, entry.email)) {
|
||||||
|
entries.push(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
deactUsersPromise = Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all([usersPromise, deactUsersPromise]);
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildUser(user: any) {
|
||||||
|
const entry = new UserEntry();
|
||||||
|
entry.externalId = user.id;
|
||||||
|
entry.referenceId = user.id;
|
||||||
|
entry.email = user.profile.email != null ? user.profile.email.trim().toLowerCase() : null;
|
||||||
|
entry.deleted = user.status === "DEPROVISIONED";
|
||||||
|
entry.disabled = user.status === "SUSPENDED";
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getGroups(
|
||||||
|
force: boolean,
|
||||||
|
setFilter: [boolean, Set<string>]
|
||||||
|
): Promise<GroupEntry[]> {
|
||||||
|
const entries: GroupEntry[] = [];
|
||||||
|
const lastSync = await this.stateService.getLastGroupSync();
|
||||||
|
const oktaFilter = this.buildOktaFilter(this.syncConfig.groupFilter, force, lastSync);
|
||||||
|
|
||||||
|
this.logService.info("Querying groups.");
|
||||||
|
await this.apiGetMany("groups?filter=" + this.encodeUrlParameter(oktaFilter)).then(
|
||||||
|
async (groups: any[]) => {
|
||||||
|
for (const group of groups.filter(
|
||||||
|
(g) => !this.filterOutResult(setFilter, g.profile.name)
|
||||||
|
)) {
|
||||||
|
const entry = await this.buildGroup(group);
|
||||||
|
if (entry != null) {
|
||||||
|
entries.push(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async buildGroup(group: any): Promise<GroupEntry> {
|
||||||
|
const entry = new GroupEntry();
|
||||||
|
entry.externalId = group.id;
|
||||||
|
entry.referenceId = group.id;
|
||||||
|
entry.name = group.profile.name;
|
||||||
|
|
||||||
|
// throttle some to avoid rate limiting
|
||||||
|
const neededDelay =
|
||||||
|
DelayBetweenBuildGroupCallsInMilliseconds - (Date.now() - this.lastBuildGroupCall);
|
||||||
|
if (neededDelay > 0) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, neededDelay));
|
||||||
|
}
|
||||||
|
this.lastBuildGroupCall = Date.now();
|
||||||
|
|
||||||
|
await this.apiGetMany("groups/" + group.id + "/users").then((users: any[]) => {
|
||||||
|
for (const user of users) {
|
||||||
|
entry.userMemberExternalIds.add(user.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildOktaFilter(baseFilter: string, force: boolean, lastSync: Date) {
|
||||||
|
baseFilter = this.createDirectoryQuery(baseFilter);
|
||||||
|
baseFilter = baseFilter == null || baseFilter.trim() === "" ? null : baseFilter;
|
||||||
|
if (force || lastSync == null) {
|
||||||
|
return baseFilter;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedFilter = 'lastUpdated gt "' + lastSync.toISOString() + '"';
|
||||||
|
if (baseFilter == null) {
|
||||||
|
return updatedFilter;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "(" + baseFilter + ") and " + updatedFilter;
|
||||||
|
}
|
||||||
|
|
||||||
|
private encodeUrlParameter(filter: string): string {
|
||||||
|
return filter == null ? "" : encodeURIComponent(filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async apiGetCall(url: string): Promise<[any, Map<string, string | string[]>]> {
|
||||||
|
const u = new URL(url);
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
https.get(
|
||||||
|
{
|
||||||
|
hostname: u.hostname,
|
||||||
|
path: u.pathname + u.search,
|
||||||
|
port: 443,
|
||||||
|
headers: {
|
||||||
|
Authorization: "SSWS " + this.dirConfig.token,
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
(res) => {
|
||||||
|
let body = "";
|
||||||
|
|
||||||
|
res.on("data", (chunk) => {
|
||||||
|
body += chunk;
|
||||||
|
});
|
||||||
|
|
||||||
|
res.on("end", () => {
|
||||||
|
if (res.statusCode !== 200) {
|
||||||
|
resolve(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseJson = JSON.parse(body);
|
||||||
|
if (res.headers != null) {
|
||||||
|
const headersMap = new Map<string, string | string[]>();
|
||||||
|
for (const key in res.headers) {
|
||||||
|
if (res.headers.hasOwnProperty(key)) {
|
||||||
|
const val = res.headers[key];
|
||||||
|
headersMap.set(key.toLowerCase(), val);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
resolve([responseJson, headersMap]);
|
||||||
// Deactivated users have to be queried for separately, only when no filter is provided in the first query
|
return;
|
||||||
let deactUsersPromise: any;
|
|
||||||
if (oktaFilter == null || oktaFilter.indexOf('lastUpdated ') === -1) {
|
|
||||||
let deactOktaFilter = 'status eq "DEPROVISIONED"';
|
|
||||||
if (oktaFilter != null) {
|
|
||||||
deactOktaFilter = '(' + oktaFilter + ') and ' + deactOktaFilter;
|
|
||||||
}
|
}
|
||||||
deactUsersPromise = this.apiGetMany('users?filter=' + this.encodeUrlParameter(deactOktaFilter))
|
resolve([responseJson, null]);
|
||||||
.then((users: any[]) => {
|
});
|
||||||
for (const user of users) {
|
|
||||||
const entry = this.buildUser(user);
|
|
||||||
if (entry != null && !this.filterOutResult(setFilter, entry.email)) {
|
|
||||||
entries.push(entry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
deactUsersPromise = Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.all([usersPromise, deactUsersPromise]);
|
res.on("error", () => {
|
||||||
return entries;
|
resolve(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async apiGetMany(endpoint: string, currentData: any[] = []): Promise<any[]> {
|
||||||
|
const url =
|
||||||
|
endpoint.indexOf("https://") === 0 ? endpoint : `${this.dirConfig.orgUrl}/api/v1/${endpoint}`;
|
||||||
|
const response = await this.apiGetCall(url);
|
||||||
|
if (response == null || response[0] == null || !Array.isArray(response[0])) {
|
||||||
|
throw new Error("API call failed.");
|
||||||
}
|
}
|
||||||
|
if (response[0].length === 0) {
|
||||||
private buildUser(user: any) {
|
return currentData;
|
||||||
const entry = new UserEntry();
|
|
||||||
entry.externalId = user.id;
|
|
||||||
entry.referenceId = user.id;
|
|
||||||
entry.email = user.profile.email != null ? user.profile.email.trim().toLowerCase() : null;
|
|
||||||
entry.deleted = user.status === 'DEPROVISIONED';
|
|
||||||
entry.disabled = user.status === 'SUSPENDED';
|
|
||||||
return entry;
|
|
||||||
}
|
}
|
||||||
|
currentData = currentData.concat(response[0]);
|
||||||
private async getGroups(force: boolean, setFilter: [boolean, Set<string>]): Promise<GroupEntry[]> {
|
if (response[1] == null) {
|
||||||
const entries: GroupEntry[] = [];
|
return currentData;
|
||||||
const lastSync = await this.configurationService.getLastGroupSyncDate();
|
|
||||||
const oktaFilter = this.buildOktaFilter(this.syncConfig.groupFilter, force, lastSync);
|
|
||||||
|
|
||||||
this.logService.info('Querying groups.');
|
|
||||||
await this.apiGetMany('groups?filter=' + this.encodeUrlParameter(oktaFilter)).then(async (groups: any[]) => {
|
|
||||||
for (const group of groups.filter(g => !this.filterOutResult(setFilter, g.profile.name))) {
|
|
||||||
const entry = await this.buildGroup(group);
|
|
||||||
if (entry != null) {
|
|
||||||
entries.push(entry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return entries;
|
|
||||||
}
|
}
|
||||||
|
const linkHeader = response[1].get("link");
|
||||||
private async buildGroup(group: any): Promise<GroupEntry> {
|
if (linkHeader == null || Array.isArray(linkHeader)) {
|
||||||
const entry = new GroupEntry();
|
return currentData;
|
||||||
entry.externalId = group.id;
|
|
||||||
entry.referenceId = group.id;
|
|
||||||
entry.name = group.profile.name;
|
|
||||||
|
|
||||||
// throttle some to avoid rate limiting
|
|
||||||
const neededDelay = DelayBetweenBuildGroupCallsInMilliseconds - (Date.now() - this.lastBuildGroupCall);
|
|
||||||
if (neededDelay > 0) {
|
|
||||||
await new Promise(resolve =>
|
|
||||||
setTimeout(resolve, neededDelay));
|
|
||||||
}
|
|
||||||
this.lastBuildGroupCall = Date.now();
|
|
||||||
|
|
||||||
await this.apiGetMany('groups/' + group.id + '/users').then((users: any[]) => {
|
|
||||||
for (const user of users) {
|
|
||||||
entry.userMemberExternalIds.add(user.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return entry;
|
|
||||||
}
|
}
|
||||||
|
let nextLink: string = null;
|
||||||
private buildOktaFilter(baseFilter: string, force: boolean, lastSync: Date) {
|
const linkHeaderParts = linkHeader.split(",");
|
||||||
baseFilter = this.createDirectoryQuery(baseFilter);
|
for (const part of linkHeaderParts) {
|
||||||
baseFilter = baseFilter == null || baseFilter.trim() === '' ? null : baseFilter;
|
if (part.indexOf('; rel="next"') > -1) {
|
||||||
if (force || lastSync == null) {
|
const subParts = part.split(";");
|
||||||
return baseFilter;
|
if (subParts.length > 0 && subParts[0].indexOf("https://") > -1) {
|
||||||
|
nextLink = subParts[0].replace(">", "").replace("<", "").trim();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
const updatedFilter = 'lastUpdated gt "' + lastSync.toISOString() + '"';
|
|
||||||
if (baseFilter == null) {
|
|
||||||
return updatedFilter;
|
|
||||||
}
|
|
||||||
|
|
||||||
return '(' + baseFilter + ') and ' + updatedFilter;
|
|
||||||
}
|
}
|
||||||
|
if (nextLink == null) {
|
||||||
private encodeUrlParameter(filter: string): string {
|
return currentData;
|
||||||
return filter == null ? '' : encodeURIComponent(filter);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async apiGetCall(url: string): Promise<[any, Map<string, string | string[]>]> {
|
|
||||||
const u = new URL(url);
|
|
||||||
return new Promise(resolve => {
|
|
||||||
https.get({
|
|
||||||
hostname: u.hostname,
|
|
||||||
path: u.pathname + u.search,
|
|
||||||
port: 443,
|
|
||||||
headers: {
|
|
||||||
Authorization: 'SSWS ' + this.dirConfig.token,
|
|
||||||
Accept: 'application/json',
|
|
||||||
},
|
|
||||||
}, res => {
|
|
||||||
let body = '';
|
|
||||||
|
|
||||||
res.on('data', chunk => {
|
|
||||||
body += chunk;
|
|
||||||
});
|
|
||||||
|
|
||||||
res.on('end', () => {
|
|
||||||
if (res.statusCode !== 200) {
|
|
||||||
resolve(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const responseJson = JSON.parse(body);
|
|
||||||
if (res.headers != null) {
|
|
||||||
const headersMap = new Map<string, string | string[]>();
|
|
||||||
for (const key in res.headers) {
|
|
||||||
if (res.headers.hasOwnProperty(key)) {
|
|
||||||
const val = res.headers[key];
|
|
||||||
headersMap.set(key.toLowerCase(), val);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resolve([responseJson, headersMap]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resolve([responseJson, null]);
|
|
||||||
});
|
|
||||||
|
|
||||||
res.on('error', () => {
|
|
||||||
resolve(null);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async apiGetMany(endpoint: string, currentData: any[] = []): Promise<any[]> {
|
|
||||||
const url = endpoint.indexOf('https://') === 0 ? endpoint : `${this.dirConfig.orgUrl}/api/v1/${endpoint}`;
|
|
||||||
const response = await this.apiGetCall(url);
|
|
||||||
if (response == null || response[0] == null || !Array.isArray(response[0])) {
|
|
||||||
throw new Error('API call failed.');
|
|
||||||
}
|
|
||||||
if (response[0].length === 0) {
|
|
||||||
return currentData;
|
|
||||||
}
|
|
||||||
currentData = currentData.concat(response[0]);
|
|
||||||
if (response[1] == null) {
|
|
||||||
return currentData;
|
|
||||||
}
|
|
||||||
const linkHeader = response[1].get('link');
|
|
||||||
if (linkHeader == null || Array.isArray(linkHeader)) {
|
|
||||||
return currentData;
|
|
||||||
}
|
|
||||||
let nextLink: string = null;
|
|
||||||
const linkHeaderParts = linkHeader.split(',');
|
|
||||||
for (const part of linkHeaderParts) {
|
|
||||||
if (part.indexOf('; rel="next"') > -1) {
|
|
||||||
const subParts = part.split(';');
|
|
||||||
if (subParts.length > 0 && subParts[0].indexOf('https://') > -1) {
|
|
||||||
nextLink = subParts[0].replace('>', '').replace('<', '').trim();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (nextLink == null) {
|
|
||||||
return currentData;
|
|
||||||
}
|
|
||||||
return this.apiGetMany(nextLink, currentData);
|
|
||||||
}
|
}
|
||||||
|
return this.apiGetMany(nextLink, currentData);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,195 +1,209 @@
|
|||||||
import { DirectoryType } from '../enums/directoryType';
|
import { DirectoryType } from "../enums/directoryType";
|
||||||
|
|
||||||
import { GroupEntry } from '../models/groupEntry';
|
import { GroupEntry } from "../models/groupEntry";
|
||||||
import { OneLoginConfiguration } from '../models/oneLoginConfiguration';
|
import { OneLoginConfiguration } from "../models/oneLoginConfiguration";
|
||||||
import { SyncConfiguration } from '../models/syncConfiguration';
|
import { SyncConfiguration } from "../models/syncConfiguration";
|
||||||
import { UserEntry } from '../models/userEntry';
|
import { UserEntry } from "../models/userEntry";
|
||||||
|
|
||||||
import { BaseDirectoryService } from './baseDirectory.service';
|
import { BaseDirectoryService } from "./baseDirectory.service";
|
||||||
import { ConfigurationService } from './configuration.service';
|
import { IDirectoryService } from "./directory.service";
|
||||||
import { IDirectoryService } from './directory.service';
|
|
||||||
|
|
||||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
import { LogService } from 'jslib-common/abstractions/log.service';
|
import { LogService } from "jslib-common/abstractions/log.service";
|
||||||
|
import { StateService } from "../abstractions/state.service";
|
||||||
|
|
||||||
// Basic email validation: something@something.something
|
// Basic email validation: something@something.something
|
||||||
const ValidEmailRegex = /^\S+@\S+\.\S+$/;
|
const ValidEmailRegex = /^\S+@\S+\.\S+$/;
|
||||||
|
|
||||||
export class OneLoginDirectoryService extends BaseDirectoryService implements IDirectoryService {
|
export class OneLoginDirectoryService extends BaseDirectoryService implements IDirectoryService {
|
||||||
private dirConfig: OneLoginConfiguration;
|
private dirConfig: OneLoginConfiguration;
|
||||||
private syncConfig: SyncConfiguration;
|
private syncConfig: SyncConfiguration;
|
||||||
private accessToken: string;
|
private accessToken: string;
|
||||||
private allUsers: any[] = [];
|
private allUsers: any[] = [];
|
||||||
|
|
||||||
constructor(private configurationService: ConfigurationService, private logService: LogService,
|
constructor(
|
||||||
private i18nService: I18nService) {
|
private logService: LogService,
|
||||||
super();
|
private i18nService: I18nService,
|
||||||
|
private stateService: StateService
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> {
|
||||||
|
const type = await this.stateService.getDirectoryType();
|
||||||
|
if (type !== DirectoryType.OneLogin) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> {
|
this.dirConfig = await this.stateService.getDirectory<OneLoginConfiguration>(
|
||||||
const type = await this.configurationService.getDirectoryType();
|
DirectoryType.OneLogin
|
||||||
if (type !== DirectoryType.OneLogin) {
|
);
|
||||||
return;
|
if (this.dirConfig == null) {
|
||||||
}
|
return;
|
||||||
|
|
||||||
this.dirConfig = await this.configurationService.getDirectory<OneLoginConfiguration>(DirectoryType.OneLogin);
|
|
||||||
if (this.dirConfig == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.syncConfig = await this.configurationService.getSync();
|
|
||||||
if (this.syncConfig == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.dirConfig.clientId == null || this.dirConfig.clientSecret == null) {
|
|
||||||
throw new Error(this.i18nService.t('dirConfigIncomplete'));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.accessToken = await this.getAccessToken();
|
|
||||||
if (this.accessToken == null) {
|
|
||||||
throw new Error('Could not get access token');
|
|
||||||
}
|
|
||||||
|
|
||||||
let users: UserEntry[];
|
|
||||||
if (this.syncConfig.users) {
|
|
||||||
users = await this.getUsers(force);
|
|
||||||
}
|
|
||||||
|
|
||||||
let groups: GroupEntry[];
|
|
||||||
if (this.syncConfig.groups) {
|
|
||||||
const setFilter = this.createCustomSet(this.syncConfig.groupFilter);
|
|
||||||
groups = await this.getGroups(this.forceGroup(force, users), setFilter);
|
|
||||||
users = this.filterUsersFromGroupsSet(users, groups, setFilter, this.syncConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
return [groups, users];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getUsers(force: boolean): Promise<UserEntry[]> {
|
this.syncConfig = await this.stateService.getSync();
|
||||||
const entries: UserEntry[] = [];
|
if (this.syncConfig == null) {
|
||||||
const query = this.createDirectoryQuery(this.syncConfig.userFilter);
|
return;
|
||||||
const setFilter = this.createCustomSet(this.syncConfig.userFilter);
|
|
||||||
this.logService.info('Querying users.');
|
|
||||||
this.allUsers = await this.apiGetMany('users' + (query != null ? '?' + query : ''));
|
|
||||||
this.allUsers.forEach(user => {
|
|
||||||
const entry = this.buildUser(user);
|
|
||||||
if (entry != null && !this.filterOutResult(setFilter, entry.email)) {
|
|
||||||
entries.push(entry);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return Promise.resolve(entries);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildUser(user: any) {
|
if (this.dirConfig.clientId == null || this.dirConfig.clientSecret == null) {
|
||||||
const entry = new UserEntry();
|
throw new Error(this.i18nService.t("dirConfigIncomplete"));
|
||||||
entry.externalId = user.id;
|
|
||||||
entry.referenceId = user.id;
|
|
||||||
entry.deleted = false;
|
|
||||||
entry.disabled = user.status === 2;
|
|
||||||
entry.email = user.email;
|
|
||||||
if (!this.validEmailAddress(entry.email) && user.username != null && user.username !== '') {
|
|
||||||
if (this.validEmailAddress(user.username)) {
|
|
||||||
entry.email = user.username;
|
|
||||||
} else if (this.syncConfig.useEmailPrefixSuffix && this.syncConfig.emailSuffix != null) {
|
|
||||||
entry.email = user.username + this.syncConfig.emailSuffix;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (entry.email != null) {
|
|
||||||
entry.email = entry.email.trim().toLowerCase();
|
|
||||||
}
|
|
||||||
if (!this.validEmailAddress(entry.email)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return entry;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getGroups(force: boolean, setFilter: [boolean, Set<string>]): Promise<GroupEntry[]> {
|
this.accessToken = await this.getAccessToken();
|
||||||
const entries: GroupEntry[] = [];
|
if (this.accessToken == null) {
|
||||||
const query = this.createDirectoryQuery(this.syncConfig.groupFilter);
|
throw new Error("Could not get access token");
|
||||||
this.logService.info('Querying groups.');
|
|
||||||
const roles = await this.apiGetMany('roles' + (query != null ? '?' + query : ''));
|
|
||||||
roles.forEach(role => {
|
|
||||||
const entry = this.buildGroup(role);
|
|
||||||
if (entry != null && !this.filterOutResult(setFilter, entry.name)) {
|
|
||||||
entries.push(entry);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return Promise.resolve(entries);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildGroup(group: any) {
|
let users: UserEntry[];
|
||||||
const entry = new GroupEntry();
|
if (this.syncConfig.users) {
|
||||||
entry.externalId = group.id;
|
users = await this.getUsers(force);
|
||||||
entry.referenceId = group.id;
|
|
||||||
entry.name = group.name;
|
|
||||||
|
|
||||||
if (this.allUsers != null) {
|
|
||||||
this.allUsers.forEach(user => {
|
|
||||||
if (user.role_id != null && user.role_id.indexOf(entry.referenceId) > -1) {
|
|
||||||
entry.userMemberExternalIds.add(user.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return entry;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getAccessToken() {
|
let groups: GroupEntry[];
|
||||||
const response = await fetch(`https://api.${this.dirConfig.region}.onelogin.com/auth/oauth2/v2/token`, {
|
if (this.syncConfig.groups) {
|
||||||
method: 'POST',
|
const setFilter = this.createCustomSet(this.syncConfig.groupFilter);
|
||||||
headers: new Headers({
|
groups = await this.getGroups(this.forceGroup(force, users), setFilter);
|
||||||
'Authorization': 'Basic ' + btoa(this.dirConfig.clientId + ':' + this.dirConfig.clientSecret),
|
users = this.filterUsersFromGroupsSet(users, groups, setFilter, this.syncConfig);
|
||||||
'Content-Type': 'application/json; charset=utf-8',
|
|
||||||
'Accept': 'application/json',
|
|
||||||
}),
|
|
||||||
body: JSON.stringify({
|
|
||||||
grant_type: 'client_credentials',
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
if (response.status === 200) {
|
|
||||||
const responseJson = await response.json();
|
|
||||||
if (responseJson.access_token != null) {
|
|
||||||
return responseJson.access_token;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async apiGetCall(url: string): Promise<any> {
|
return [groups, users];
|
||||||
const req: RequestInit = {
|
}
|
||||||
method: 'GET',
|
|
||||||
headers: new Headers({
|
private async getUsers(force: boolean): Promise<UserEntry[]> {
|
||||||
Authorization: 'bearer:' + this.accessToken,
|
const entries: UserEntry[] = [];
|
||||||
Accept: 'application/json',
|
const query = this.createDirectoryQuery(this.syncConfig.userFilter);
|
||||||
}),
|
const setFilter = this.createCustomSet(this.syncConfig.userFilter);
|
||||||
};
|
this.logService.info("Querying users.");
|
||||||
const response = await fetch(new Request(url, req));
|
this.allUsers = await this.apiGetMany("users" + (query != null ? "?" + query : ""));
|
||||||
if (response.status === 200) {
|
this.allUsers.forEach((user) => {
|
||||||
const responseJson = await response.json();
|
const entry = this.buildUser(user);
|
||||||
return responseJson;
|
if (entry != null && !this.filterOutResult(setFilter, entry.email)) {
|
||||||
|
entries.push(entry);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Promise.resolve(entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildUser(user: any) {
|
||||||
|
const entry = new UserEntry();
|
||||||
|
entry.externalId = user.id;
|
||||||
|
entry.referenceId = user.id;
|
||||||
|
entry.deleted = false;
|
||||||
|
entry.disabled = user.status === 2;
|
||||||
|
entry.email = user.email;
|
||||||
|
if (!this.validEmailAddress(entry.email) && user.username != null && user.username !== "") {
|
||||||
|
if (this.validEmailAddress(user.username)) {
|
||||||
|
entry.email = user.username;
|
||||||
|
} else if (this.syncConfig.useEmailPrefixSuffix && this.syncConfig.emailSuffix != null) {
|
||||||
|
entry.email = user.username + this.syncConfig.emailSuffix;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (entry.email != null) {
|
||||||
|
entry.email = entry.email.trim().toLowerCase();
|
||||||
|
}
|
||||||
|
if (!this.validEmailAddress(entry.email)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getGroups(
|
||||||
|
force: boolean,
|
||||||
|
setFilter: [boolean, Set<string>]
|
||||||
|
): Promise<GroupEntry[]> {
|
||||||
|
const entries: GroupEntry[] = [];
|
||||||
|
const query = this.createDirectoryQuery(this.syncConfig.groupFilter);
|
||||||
|
this.logService.info("Querying groups.");
|
||||||
|
const roles = await this.apiGetMany("roles" + (query != null ? "?" + query : ""));
|
||||||
|
roles.forEach((role) => {
|
||||||
|
const entry = this.buildGroup(role);
|
||||||
|
if (entry != null && !this.filterOutResult(setFilter, entry.name)) {
|
||||||
|
entries.push(entry);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Promise.resolve(entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildGroup(group: any) {
|
||||||
|
const entry = new GroupEntry();
|
||||||
|
entry.externalId = group.id;
|
||||||
|
entry.referenceId = group.id;
|
||||||
|
entry.name = group.name;
|
||||||
|
|
||||||
|
if (this.allUsers != null) {
|
||||||
|
this.allUsers.forEach((user) => {
|
||||||
|
if (user.role_id != null && user.role_id.indexOf(entry.referenceId) > -1) {
|
||||||
|
entry.userMemberExternalIds.add(user.id);
|
||||||
}
|
}
|
||||||
return null;
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async apiGetMany(endpoint: string, currentData: any[] = []): Promise<any[]> {
|
return entry;
|
||||||
const url = endpoint.indexOf('https://') === 0 ? endpoint :
|
}
|
||||||
`https://api.${this.dirConfig.region}.onelogin.com/api/1/${endpoint}`;
|
|
||||||
const response = await this.apiGetCall(url);
|
|
||||||
if (response == null || response.status == null || response.data == null) {
|
|
||||||
return currentData;
|
|
||||||
}
|
|
||||||
if (response.status.code !== 200) {
|
|
||||||
throw new Error('API call failed.');
|
|
||||||
}
|
|
||||||
currentData = currentData.concat(response.data);
|
|
||||||
if (response.pagination == null || response.pagination.next_link == null) {
|
|
||||||
return currentData;
|
|
||||||
}
|
|
||||||
return this.apiGetMany(response.pagination.next_link, currentData);
|
|
||||||
}
|
|
||||||
|
|
||||||
private validEmailAddress(email: string) {
|
private async getAccessToken() {
|
||||||
return email != null && email !== '' && ValidEmailRegex.test(email);
|
const response = await fetch(
|
||||||
|
`https://api.${this.dirConfig.region}.onelogin.com/auth/oauth2/v2/token`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: new Headers({
|
||||||
|
Authorization:
|
||||||
|
"Basic " + btoa(this.dirConfig.clientId + ":" + this.dirConfig.clientSecret),
|
||||||
|
"Content-Type": "application/json; charset=utf-8",
|
||||||
|
Accept: "application/json",
|
||||||
|
}),
|
||||||
|
body: JSON.stringify({
|
||||||
|
grant_type: "client_credentials",
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (response.status === 200) {
|
||||||
|
const responseJson = await response.json();
|
||||||
|
if (responseJson.access_token != null) {
|
||||||
|
return responseJson.access_token;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async apiGetCall(url: string): Promise<any> {
|
||||||
|
const req: RequestInit = {
|
||||||
|
method: "GET",
|
||||||
|
headers: new Headers({
|
||||||
|
Authorization: "bearer:" + this.accessToken,
|
||||||
|
Accept: "application/json",
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
const response = await fetch(new Request(url, req));
|
||||||
|
if (response.status === 200) {
|
||||||
|
const responseJson = await response.json();
|
||||||
|
return responseJson;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async apiGetMany(endpoint: string, currentData: any[] = []): Promise<any[]> {
|
||||||
|
const url =
|
||||||
|
endpoint.indexOf("https://") === 0
|
||||||
|
? endpoint
|
||||||
|
: `https://api.${this.dirConfig.region}.onelogin.com/api/1/${endpoint}`;
|
||||||
|
const response = await this.apiGetCall(url);
|
||||||
|
if (response == null || response.status == null || response.data == null) {
|
||||||
|
return currentData;
|
||||||
|
}
|
||||||
|
if (response.status.code !== 200) {
|
||||||
|
throw new Error("API call failed.");
|
||||||
|
}
|
||||||
|
currentData = currentData.concat(response.data);
|
||||||
|
if (response.pagination == null || response.pagination.next_link == null) {
|
||||||
|
return currentData;
|
||||||
|
}
|
||||||
|
return this.apiGetMany(response.pagination.next_link, currentData);
|
||||||
|
}
|
||||||
|
|
||||||
|
private validEmailAddress(email: string) {
|
||||||
|
return email != null && email !== "" && ValidEmailRegex.test(email);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
590
src/services/state.service.ts
Normal file
590
src/services/state.service.ts
Normal file
@@ -0,0 +1,590 @@
|
|||||||
|
import { StateService as BaseStateService } from "jslib-common/services/state.service";
|
||||||
|
|
||||||
|
import { GlobalState } from "jslib-common/models/domain/globalState";
|
||||||
|
import { StorageOptions } from "jslib-common/models/domain/storageOptions";
|
||||||
|
|
||||||
|
import { StateFactory } from "jslib-common/factories/stateFactory";
|
||||||
|
|
||||||
|
import { Account } from "src/models/account";
|
||||||
|
import { AzureConfiguration } from "src/models/azureConfiguration";
|
||||||
|
import { GSuiteConfiguration } from "src/models/gsuiteConfiguration";
|
||||||
|
import { IConfiguration } from "src/models/IConfiguration";
|
||||||
|
import { LdapConfiguration } from "src/models/ldapConfiguration";
|
||||||
|
import { OktaConfiguration } from "src/models/oktaConfiguration";
|
||||||
|
import { OneLoginConfiguration } from "src/models/oneLoginConfiguration";
|
||||||
|
import { SyncConfiguration } from "src/models/syncConfiguration";
|
||||||
|
|
||||||
|
import { LogService } from "jslib-common/abstractions/log.service";
|
||||||
|
import { StateMigrationService } from "jslib-common/abstractions/stateMigration.service";
|
||||||
|
import { StorageService } from "jslib-common/abstractions/storage.service";
|
||||||
|
|
||||||
|
import { StateService as StateServiceAbstraction } from "src/abstractions/state.service";
|
||||||
|
|
||||||
|
import { DirectoryType } from "src/enums/directoryType";
|
||||||
|
|
||||||
|
const SecureStorageKeys = {
|
||||||
|
ldap: "ldapPassword",
|
||||||
|
gsuite: "gsuitePrivateKey",
|
||||||
|
azure: "azureKey",
|
||||||
|
okta: "oktaToken",
|
||||||
|
oneLogin: "oneLoginClientSecret",
|
||||||
|
userDelta: "userDeltaToken",
|
||||||
|
groupDelta: "groupDeltaToken",
|
||||||
|
lastUserSync: "lastUserSync",
|
||||||
|
lastGroupSync: "lastGroupSync",
|
||||||
|
lastSyncHash: "lastSyncHash",
|
||||||
|
};
|
||||||
|
|
||||||
|
const keys = {
|
||||||
|
tempAccountSettings: "tempAccountSettings",
|
||||||
|
tempDirectoryConfigs: "tempDirectoryConfigs",
|
||||||
|
tempDirectorySettings: "tempDirectorySettings",
|
||||||
|
};
|
||||||
|
|
||||||
|
const StoredSecurely = "[STORED SECURELY]";
|
||||||
|
|
||||||
|
export class StateService
|
||||||
|
extends BaseStateService<GlobalState, Account>
|
||||||
|
implements StateServiceAbstraction
|
||||||
|
{
|
||||||
|
constructor(
|
||||||
|
protected storageService: StorageService,
|
||||||
|
protected secureStorageService: StorageService,
|
||||||
|
protected logService: LogService,
|
||||||
|
protected stateMigrationService: StateMigrationService,
|
||||||
|
private useSecureStorageForSecrets = true,
|
||||||
|
protected stateFactory: StateFactory<GlobalState, Account>
|
||||||
|
) {
|
||||||
|
super(storageService, secureStorageService, logService, stateMigrationService, stateFactory);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDirectory<T extends IConfiguration>(type: DirectoryType): Promise<T> {
|
||||||
|
const config = await this.getConfiguration(type);
|
||||||
|
if (config == null) {
|
||||||
|
return config as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.useSecureStorageForSecrets) {
|
||||||
|
switch (type) {
|
||||||
|
case DirectoryType.Ldap:
|
||||||
|
(config as any).password = await this.getLdapKey();
|
||||||
|
break;
|
||||||
|
case DirectoryType.AzureActiveDirectory:
|
||||||
|
(config as any).key = await this.getAzureKey();
|
||||||
|
break;
|
||||||
|
case DirectoryType.Okta:
|
||||||
|
(config as any).token = await this.getOktaKey();
|
||||||
|
break;
|
||||||
|
case DirectoryType.GSuite:
|
||||||
|
(config as any).privateKey = await this.getGsuiteKey();
|
||||||
|
break;
|
||||||
|
case DirectoryType.OneLogin:
|
||||||
|
(config as any).clientSecret = await this.getOneLoginKey();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return config as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setDirectory(
|
||||||
|
type: DirectoryType,
|
||||||
|
config:
|
||||||
|
| LdapConfiguration
|
||||||
|
| GSuiteConfiguration
|
||||||
|
| AzureConfiguration
|
||||||
|
| OktaConfiguration
|
||||||
|
| OneLoginConfiguration
|
||||||
|
): Promise<any> {
|
||||||
|
const savedConfig: any = Object.assign({}, config);
|
||||||
|
if (this.useSecureStorageForSecrets) {
|
||||||
|
switch (type) {
|
||||||
|
case DirectoryType.Ldap:
|
||||||
|
await this.setLdapKey(savedConfig.password);
|
||||||
|
savedConfig.password = StoredSecurely;
|
||||||
|
await this.setLdapConfiguration(savedConfig);
|
||||||
|
break;
|
||||||
|
case DirectoryType.AzureActiveDirectory:
|
||||||
|
await this.setAzureKey(savedConfig.key);
|
||||||
|
savedConfig.key = StoredSecurely;
|
||||||
|
await this.setAzureConfiguration(savedConfig);
|
||||||
|
break;
|
||||||
|
case DirectoryType.Okta:
|
||||||
|
await this.setOktaKey(savedConfig.token);
|
||||||
|
savedConfig.token = StoredSecurely;
|
||||||
|
await this.setOktaConfiguration(savedConfig);
|
||||||
|
break;
|
||||||
|
case DirectoryType.GSuite:
|
||||||
|
if (savedConfig.privateKey == null) {
|
||||||
|
await this.setGsuiteKey(null);
|
||||||
|
} else {
|
||||||
|
(config as GSuiteConfiguration).privateKey = savedConfig.privateKey =
|
||||||
|
savedConfig.privateKey.replace(/\\n/g, "\n");
|
||||||
|
await this.setGsuiteKey(savedConfig.privateKey);
|
||||||
|
savedConfig.privateKey = StoredSecurely;
|
||||||
|
}
|
||||||
|
await this.setGsuiteConfiguration(savedConfig);
|
||||||
|
break;
|
||||||
|
case DirectoryType.OneLogin:
|
||||||
|
await this.setOneLoginKey(savedConfig.clientSecret);
|
||||||
|
savedConfig.clientSecret = StoredSecurely;
|
||||||
|
await this.setOneLoginConfiguration(savedConfig);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getLdapKey(options?: StorageOptions): Promise<string> {
|
||||||
|
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
|
||||||
|
if (options?.userId == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return await this.secureStorageService.get<string>(
|
||||||
|
`${options.userId}_${SecureStorageKeys.ldap}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setLdapKey(value: string, options?: StorageOptions): Promise<void> {
|
||||||
|
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
|
||||||
|
if (options?.userId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.secureStorageService.save(
|
||||||
|
`${options.userId}_${SecureStorageKeys.ldap}`,
|
||||||
|
value,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getGsuiteKey(options?: StorageOptions): Promise<string> {
|
||||||
|
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
|
||||||
|
if (options?.userId == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return await this.secureStorageService.get<string>(
|
||||||
|
`${options.userId}_${SecureStorageKeys.gsuite}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setGsuiteKey(value: string, options?: StorageOptions): Promise<void> {
|
||||||
|
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
|
||||||
|
if (options?.userId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.secureStorageService.save(
|
||||||
|
`${options.userId}_${SecureStorageKeys.gsuite}`,
|
||||||
|
value,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAzureKey(options?: StorageOptions): Promise<string> {
|
||||||
|
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
|
||||||
|
if (options?.userId == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return await this.secureStorageService.get<string>(
|
||||||
|
`${options.userId}_${SecureStorageKeys.azure}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setAzureKey(value: string, options?: StorageOptions): Promise<void> {
|
||||||
|
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
|
||||||
|
if (options?.userId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.secureStorageService.save(
|
||||||
|
`${options.userId}_${SecureStorageKeys.azure}`,
|
||||||
|
value,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOktaKey(options?: StorageOptions): Promise<string> {
|
||||||
|
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
|
||||||
|
if (options?.userId == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return await this.secureStorageService.get<string>(
|
||||||
|
`${options.userId}_${SecureStorageKeys.okta}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setOktaKey(value: string, options?: StorageOptions): Promise<void> {
|
||||||
|
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
|
||||||
|
if (options?.userId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.secureStorageService.save(
|
||||||
|
`${options.userId}_${SecureStorageKeys.okta}`,
|
||||||
|
value,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOneLoginKey(options?: StorageOptions): Promise<string> {
|
||||||
|
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
|
||||||
|
if (options?.userId == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return await this.secureStorageService.get<string>(
|
||||||
|
`${options.userId}_${SecureStorageKeys.oneLogin}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setOneLoginKey(value: string, options?: StorageOptions): Promise<void> {
|
||||||
|
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
|
||||||
|
if (options?.userId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.secureStorageService.save(
|
||||||
|
`${options.userId}_${SecureStorageKeys.oneLogin}`,
|
||||||
|
value,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserDelta(options?: StorageOptions): Promise<string> {
|
||||||
|
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
|
||||||
|
if (options?.userId == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return await this.secureStorageService.get<string>(
|
||||||
|
`${options.userId}_${SecureStorageKeys.userDelta}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setUserDelta(value: string, options?: StorageOptions): Promise<void> {
|
||||||
|
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
|
||||||
|
if (options?.userId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.secureStorageService.save(
|
||||||
|
`${options.userId}_${SecureStorageKeys.userDelta}`,
|
||||||
|
value,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getGroupDelta(options?: StorageOptions): Promise<string> {
|
||||||
|
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
|
||||||
|
if (options?.userId == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return await this.secureStorageService.get<string>(
|
||||||
|
`${options.userId}_${SecureStorageKeys.groupDelta}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setGroupDelta(value: string, options?: StorageOptions): Promise<void> {
|
||||||
|
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
|
||||||
|
if (options?.userId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.secureStorageService.save(
|
||||||
|
`${options.userId}_${SecureStorageKeys.groupDelta}`,
|
||||||
|
value,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getConfiguration(type: DirectoryType): Promise<IConfiguration> {
|
||||||
|
switch (type) {
|
||||||
|
case DirectoryType.Ldap:
|
||||||
|
return await this.getLdapConfiguration();
|
||||||
|
case DirectoryType.GSuite:
|
||||||
|
return await this.getGsuiteConfiguration();
|
||||||
|
case DirectoryType.AzureActiveDirectory:
|
||||||
|
return await this.getAzureConfiguration();
|
||||||
|
case DirectoryType.Okta:
|
||||||
|
return await this.getOktaConfiguration();
|
||||||
|
case DirectoryType.OneLogin:
|
||||||
|
return await this.getOneLoginConfiguration();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getLdapConfiguration(options?: StorageOptions): Promise<LdapConfiguration> {
|
||||||
|
return (
|
||||||
|
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
||||||
|
)?.directoryConfigurations?.ldap;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setLdapConfiguration(value: LdapConfiguration, options?: StorageOptions): Promise<void> {
|
||||||
|
const account = await this.getAccount(
|
||||||
|
this.reconcileOptions(options, await this.defaultOnDiskOptions())
|
||||||
|
);
|
||||||
|
account.directoryConfigurations.ldap = value;
|
||||||
|
await this.saveAccount(
|
||||||
|
account,
|
||||||
|
this.reconcileOptions(options, await this.defaultOnDiskOptions())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getGsuiteConfiguration(options?: StorageOptions): Promise<GSuiteConfiguration> {
|
||||||
|
return (
|
||||||
|
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
||||||
|
)?.directoryConfigurations?.gsuite;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setGsuiteConfiguration(
|
||||||
|
value: GSuiteConfiguration,
|
||||||
|
options?: StorageOptions
|
||||||
|
): Promise<void> {
|
||||||
|
const account = await this.getAccount(
|
||||||
|
this.reconcileOptions(options, await this.defaultOnDiskOptions())
|
||||||
|
);
|
||||||
|
account.directoryConfigurations.gsuite = value;
|
||||||
|
await this.saveAccount(
|
||||||
|
account,
|
||||||
|
this.reconcileOptions(options, await this.defaultOnDiskOptions())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAzureConfiguration(options?: StorageOptions): Promise<AzureConfiguration> {
|
||||||
|
return (
|
||||||
|
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
||||||
|
)?.directoryConfigurations?.azure;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setAzureConfiguration(value: AzureConfiguration, options?: StorageOptions): Promise<void> {
|
||||||
|
const account = await this.getAccount(
|
||||||
|
this.reconcileOptions(options, await this.defaultOnDiskOptions())
|
||||||
|
);
|
||||||
|
account.directoryConfigurations.azure = value;
|
||||||
|
await this.saveAccount(
|
||||||
|
account,
|
||||||
|
this.reconcileOptions(options, await this.defaultOnDiskOptions())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOktaConfiguration(options?: StorageOptions): Promise<OktaConfiguration> {
|
||||||
|
return (
|
||||||
|
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
||||||
|
)?.directoryConfigurations?.okta;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setOktaConfiguration(value: OktaConfiguration, options?: StorageOptions): Promise<void> {
|
||||||
|
const account = await this.getAccount(
|
||||||
|
this.reconcileOptions(options, await this.defaultOnDiskOptions())
|
||||||
|
);
|
||||||
|
account.directoryConfigurations.okta = value;
|
||||||
|
await this.saveAccount(
|
||||||
|
account,
|
||||||
|
this.reconcileOptions(options, await this.defaultOnDiskOptions())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOneLoginConfiguration(options?: StorageOptions): Promise<OneLoginConfiguration> {
|
||||||
|
return (
|
||||||
|
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
||||||
|
)?.directoryConfigurations?.oneLogin;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setOneLoginConfiguration(
|
||||||
|
value: OneLoginConfiguration,
|
||||||
|
options?: StorageOptions
|
||||||
|
): Promise<void> {
|
||||||
|
const account = await this.getAccount(
|
||||||
|
this.reconcileOptions(options, await this.defaultOnDiskOptions())
|
||||||
|
);
|
||||||
|
account.directoryConfigurations.oneLogin = value;
|
||||||
|
await this.saveAccount(
|
||||||
|
account,
|
||||||
|
this.reconcileOptions(options, await this.defaultOnDiskOptions())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOrganizationId(options?: StorageOptions): Promise<string> {
|
||||||
|
return (
|
||||||
|
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
||||||
|
)?.directorySettings?.organizationId;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setOrganizationId(value: string, options?: StorageOptions): Promise<void> {
|
||||||
|
const currentId = await this.getOrganizationId();
|
||||||
|
if (currentId !== value) {
|
||||||
|
await this.clearSyncSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
const account = await this.getAccount(
|
||||||
|
this.reconcileOptions(options, await this.defaultOnDiskOptions())
|
||||||
|
);
|
||||||
|
account.directorySettings.organizationId = value;
|
||||||
|
await this.saveAccount(
|
||||||
|
account,
|
||||||
|
this.reconcileOptions(options, await this.defaultOnDiskOptions())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSync(options?: StorageOptions): Promise<SyncConfiguration> {
|
||||||
|
return (
|
||||||
|
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
||||||
|
)?.directorySettings?.sync;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setSync(value: SyncConfiguration, options?: StorageOptions): Promise<void> {
|
||||||
|
const account = await this.getAccount(
|
||||||
|
this.reconcileOptions(options, await this.defaultOnDiskOptions())
|
||||||
|
);
|
||||||
|
account.directorySettings.sync = value;
|
||||||
|
await this.saveAccount(
|
||||||
|
account,
|
||||||
|
this.reconcileOptions(options, await this.defaultOnDiskOptions())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDirectoryType(options?: StorageOptions): Promise<DirectoryType> {
|
||||||
|
return (
|
||||||
|
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
||||||
|
)?.directorySettings?.directoryType;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setDirectoryType(value: DirectoryType, options?: StorageOptions): Promise<void> {
|
||||||
|
const currentType = await this.getDirectoryType();
|
||||||
|
if (value !== currentType) {
|
||||||
|
await this.clearSyncSettings();
|
||||||
|
}
|
||||||
|
const account = await this.getAccount(
|
||||||
|
this.reconcileOptions(options, await this.defaultOnDiskOptions())
|
||||||
|
);
|
||||||
|
account.directorySettings.directoryType = value;
|
||||||
|
await this.saveAccount(
|
||||||
|
account,
|
||||||
|
this.reconcileOptions(options, await this.defaultOnDiskOptions())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getLastUserSync(options?: StorageOptions): Promise<Date> {
|
||||||
|
const userSyncDate = (
|
||||||
|
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
||||||
|
)?.directorySettings?.lastUserSync;
|
||||||
|
return userSyncDate ? new Date(userSyncDate) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setLastUserSync(value: Date, options?: StorageOptions): Promise<void> {
|
||||||
|
const account = await this.getAccount(
|
||||||
|
this.reconcileOptions(options, await this.defaultOnDiskOptions())
|
||||||
|
);
|
||||||
|
account.directorySettings.lastUserSync = value;
|
||||||
|
await this.saveAccount(
|
||||||
|
account,
|
||||||
|
this.reconcileOptions(options, await this.defaultOnDiskOptions())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getLastGroupSync(options?: StorageOptions): Promise<Date> {
|
||||||
|
const groupSyncDate = (
|
||||||
|
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
||||||
|
)?.directorySettings?.lastGroupSync;
|
||||||
|
return groupSyncDate ? new Date(groupSyncDate) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setLastGroupSync(value: Date, options?: StorageOptions): Promise<void> {
|
||||||
|
const account = await this.getAccount(
|
||||||
|
this.reconcileOptions(options, await this.defaultOnDiskOptions())
|
||||||
|
);
|
||||||
|
account.directorySettings.lastGroupSync = value;
|
||||||
|
await this.saveAccount(
|
||||||
|
account,
|
||||||
|
this.reconcileOptions(options, await this.defaultOnDiskOptions())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getLastSyncHash(options?: StorageOptions): Promise<string> {
|
||||||
|
return (
|
||||||
|
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
||||||
|
)?.directorySettings?.lastSyncHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setLastSyncHash(value: string, options?: StorageOptions): Promise<void> {
|
||||||
|
const account = await this.getAccount(
|
||||||
|
this.reconcileOptions(options, await this.defaultOnDiskOptions())
|
||||||
|
);
|
||||||
|
account.directorySettings.lastSyncHash = value;
|
||||||
|
await this.saveAccount(
|
||||||
|
account,
|
||||||
|
this.reconcileOptions(options, await this.defaultOnDiskOptions())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSyncingDir(options?: StorageOptions): Promise<boolean> {
|
||||||
|
return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))
|
||||||
|
?.directorySettings?.syncingDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setSyncingDir(value: boolean, options?: StorageOptions): Promise<void> {
|
||||||
|
const account = await this.getAccount(
|
||||||
|
this.reconcileOptions(options, this.defaultInMemoryOptions)
|
||||||
|
);
|
||||||
|
account.directorySettings.syncingDir = value;
|
||||||
|
await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions));
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearSyncSettings(hashToo = false) {
|
||||||
|
await this.setUserDelta(null);
|
||||||
|
await this.setGroupDelta(null);
|
||||||
|
await this.setLastGroupSync(null);
|
||||||
|
await this.setLastUserSync(null);
|
||||||
|
if (hashToo) {
|
||||||
|
await this.setLastSyncHash(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async scaffoldNewAccountStorage(account: Account): Promise<void> {
|
||||||
|
await this.scaffoldNewAccountDiskStorage(account);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async scaffoldNewAccountDiskStorage(account: Account): Promise<void> {
|
||||||
|
const storedAccount = await this.getAccount(
|
||||||
|
this.reconcileOptions(
|
||||||
|
{ userId: account.profile.userId },
|
||||||
|
await this.defaultOnDiskLocalOptions()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (storedAccount != null) {
|
||||||
|
account.settings = storedAccount.settings;
|
||||||
|
account.directorySettings = storedAccount.directorySettings;
|
||||||
|
account.directoryConfigurations = storedAccount.directoryConfigurations;
|
||||||
|
} else if (await this.hasTemporaryStorage()) {
|
||||||
|
// If migrating to state V2 with an no actively authed account we store temporary data to be copied on auth - this will only be run once.
|
||||||
|
account.settings = await this.storageService.get<any>(keys.tempAccountSettings);
|
||||||
|
account.directorySettings = await this.storageService.get<any>(keys.tempDirectorySettings);
|
||||||
|
account.directoryConfigurations = await this.storageService.get<any>(
|
||||||
|
keys.tempDirectoryConfigs
|
||||||
|
);
|
||||||
|
await this.storageService.remove(keys.tempAccountSettings);
|
||||||
|
await this.storageService.remove(keys.tempDirectorySettings);
|
||||||
|
await this.storageService.remove(keys.tempDirectoryConfigs);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.storageService.save(
|
||||||
|
account.profile.userId,
|
||||||
|
account,
|
||||||
|
await this.defaultOnDiskLocalOptions()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async pushAccounts(): Promise<void> {
|
||||||
|
if (this.state?.accounts == null || Object.keys(this.state.accounts).length < 1) {
|
||||||
|
this.accounts.next(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.accounts.next(this.state.accounts);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async hasTemporaryStorage(): Promise<boolean> {
|
||||||
|
return (
|
||||||
|
(await this.storageService.has(keys.tempAccountSettings)) ||
|
||||||
|
(await this.storageService.has(keys.tempDirectorySettings)) ||
|
||||||
|
(await this.storageService.has(keys.tempDirectoryConfigs))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected resetAccount(account: Account) {
|
||||||
|
const persistentAccountInformation = {
|
||||||
|
settings: account.settings,
|
||||||
|
directorySettings: account.directorySettings,
|
||||||
|
directoryConfigurations: account.directoryConfigurations,
|
||||||
|
};
|
||||||
|
return Object.assign(this.createAccount(), persistentAccountInformation);
|
||||||
|
}
|
||||||
|
}
|
||||||
160
src/services/stateMigration.service.ts
Normal file
160
src/services/stateMigration.service.ts
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
import { StateMigrationService as BaseStateMigrationService } from "jslib-common/services/stateMigration.service";
|
||||||
|
|
||||||
|
import { StateVersion } from "jslib-common/enums/stateVersion";
|
||||||
|
|
||||||
|
import { DirectoryType } from "src/enums/directoryType";
|
||||||
|
|
||||||
|
import { Account, DirectoryConfigurations, DirectorySettings } from "src/models/account";
|
||||||
|
import { AzureConfiguration } from "src/models/azureConfiguration";
|
||||||
|
import { GSuiteConfiguration } from "src/models/gsuiteConfiguration";
|
||||||
|
import { LdapConfiguration } from "src/models/ldapConfiguration";
|
||||||
|
import { OktaConfiguration } from "src/models/oktaConfiguration";
|
||||||
|
import { OneLoginConfiguration } from "src/models/oneLoginConfiguration";
|
||||||
|
import { SyncConfiguration } from "src/models/syncConfiguration";
|
||||||
|
|
||||||
|
const SecureStorageKeys: { [key: string]: any } = {
|
||||||
|
ldap: "ldapPassword",
|
||||||
|
gsuite: "gsuitePrivateKey",
|
||||||
|
azure: "azureKey",
|
||||||
|
okta: "oktaToken",
|
||||||
|
oneLogin: "oneLoginClientSecret",
|
||||||
|
directoryConfigPrefix: "directoryConfig_",
|
||||||
|
sync: "syncConfig",
|
||||||
|
directoryType: "directoryType",
|
||||||
|
userDelta: "userDeltaToken",
|
||||||
|
groupDelta: "groupDeltaToken",
|
||||||
|
organizationId: "organizationId",
|
||||||
|
};
|
||||||
|
|
||||||
|
const Keys: { [key: string]: any } = {
|
||||||
|
entityId: "entityId",
|
||||||
|
directoryType: "directoryType",
|
||||||
|
organizationId: "organizationId",
|
||||||
|
lastUserSync: "lastUserSync",
|
||||||
|
lastGroupSync: "lastGroupSync",
|
||||||
|
lastSyncHash: "lastSyncHash",
|
||||||
|
syncingDir: "syncingDir",
|
||||||
|
syncConfig: "syncConfig",
|
||||||
|
tempDirectoryConfigs: "tempDirectoryConfigs",
|
||||||
|
tempDirectorySettings: "tempDirectorySettings",
|
||||||
|
};
|
||||||
|
|
||||||
|
const ClientKeys: { [key: string]: any } = {
|
||||||
|
clientIdOld: "clientId",
|
||||||
|
clientId: "apikey_clientId",
|
||||||
|
clientSecretOld: "clientSecret",
|
||||||
|
clientSecret: "apikey_clientSecret",
|
||||||
|
};
|
||||||
|
|
||||||
|
export class StateMigrationService extends BaseStateMigrationService {
|
||||||
|
async migrate(): Promise<void> {
|
||||||
|
let currentStateVersion = await this.getCurrentStateVersion();
|
||||||
|
while (currentStateVersion < StateVersion.Latest) {
|
||||||
|
switch (currentStateVersion) {
|
||||||
|
case StateVersion.One:
|
||||||
|
await this.migrateClientKeys();
|
||||||
|
await this.migrateStateFrom1To2();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
currentStateVersion += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: remove this migration when we are confident existing api keys are all migrated. Probably 1-2 releases.
|
||||||
|
protected async migrateClientKeys() {
|
||||||
|
const oldClientId = await this.storageService.get<string>(ClientKeys.clientIdOld);
|
||||||
|
const oldClientSecret = await this.storageService.get<string>(ClientKeys.clientSecretOld);
|
||||||
|
|
||||||
|
if (oldClientId != null) {
|
||||||
|
await this.storageService.save(ClientKeys.clientId, oldClientId);
|
||||||
|
await this.storageService.remove(ClientKeys.clientIdOld);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldClientSecret != null) {
|
||||||
|
await this.storageService.save(ClientKeys.clientSecret, oldClientSecret);
|
||||||
|
await this.storageService.remove(ClientKeys.clientSecretOld);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async migrateStateFrom1To2(useSecureStorageForSecrets: boolean = true): Promise<void> {
|
||||||
|
// Grabbing a couple of key settings before they get cleared by the base migration
|
||||||
|
const userId = await this.get<string>(Keys.entityId);
|
||||||
|
const clientId = await this.get<string>(ClientKeys.clientId);
|
||||||
|
const clientSecret = await this.get<string>(ClientKeys.clientSecret);
|
||||||
|
|
||||||
|
await super.migrateStateFrom1To2();
|
||||||
|
|
||||||
|
// Setup reusable method for clearing keys since we will want to do that regardless of if there is an active authenticated session
|
||||||
|
const clearDirectoryConnectorV1Keys = async () => {
|
||||||
|
for (const key in Keys) {
|
||||||
|
if (key == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const directoryType in DirectoryType) {
|
||||||
|
if (directoryType == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
await this.set(SecureStorageKeys.directoryConfigPrefix + directoryType, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initilize typed objects from key/value pairs in storage to either be saved temporarily until an account is authed or applied to the active account
|
||||||
|
const getDirectoryConfig = async <T>(type: DirectoryType) =>
|
||||||
|
await this.get<T>(SecureStorageKeys.directoryConfigPrefix + type);
|
||||||
|
const directoryConfigs: DirectoryConfigurations = {
|
||||||
|
ldap: await getDirectoryConfig<LdapConfiguration>(DirectoryType.Ldap),
|
||||||
|
gsuite: await getDirectoryConfig<GSuiteConfiguration>(DirectoryType.GSuite),
|
||||||
|
azure: await getDirectoryConfig<AzureConfiguration>(DirectoryType.AzureActiveDirectory),
|
||||||
|
okta: await getDirectoryConfig<OktaConfiguration>(DirectoryType.Okta),
|
||||||
|
oneLogin: await getDirectoryConfig<OneLoginConfiguration>(DirectoryType.OneLogin),
|
||||||
|
};
|
||||||
|
|
||||||
|
const directorySettings: DirectorySettings = {
|
||||||
|
directoryType: await this.get<DirectoryType>(Keys.directoryType),
|
||||||
|
organizationId: await this.get<string>(Keys.organizationId),
|
||||||
|
lastUserSync: await this.get<Date>(Keys.lastUserSync),
|
||||||
|
lastGroupSync: await this.get<Date>(Keys.lastGroupSync),
|
||||||
|
lastSyncHash: await this.get<string>(Keys.lastSyncHash),
|
||||||
|
syncingDir: await this.get<boolean>(Keys.syncingDir),
|
||||||
|
sync: await this.get<SyncConfiguration>(Keys.syncConfig),
|
||||||
|
};
|
||||||
|
|
||||||
|
// (userId == null) = no authed account, stored data temporarily to be applied and cleared on next auth
|
||||||
|
// (userId != null) = authed account known, applied stored data to it and do not save temp data
|
||||||
|
if (userId == null) {
|
||||||
|
await this.set(Keys.tempDirectoryConfigs, directoryConfigs);
|
||||||
|
await this.set(Keys.tempDirectorySettings, directorySettings);
|
||||||
|
await clearDirectoryConnectorV1Keys();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const account = await this.get<Account>(userId);
|
||||||
|
account.directoryConfigurations = directoryConfigs;
|
||||||
|
account.directorySettings = directorySettings;
|
||||||
|
account.profile = {
|
||||||
|
userId: userId,
|
||||||
|
entityId: userId,
|
||||||
|
apiKeyClientId: clientId,
|
||||||
|
};
|
||||||
|
account.clientKeys = {
|
||||||
|
clientId: clientId,
|
||||||
|
clientSecret: clientSecret,
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.set(userId, account);
|
||||||
|
await clearDirectoryConnectorV1Keys();
|
||||||
|
|
||||||
|
if (useSecureStorageForSecrets) {
|
||||||
|
for (const key in SecureStorageKeys) {
|
||||||
|
if (await this.secureStorageService.has(SecureStorageKeys[key])) {
|
||||||
|
await this.secureStorageService.save(
|
||||||
|
`${userId}_${SecureStorageKeys[key]}`,
|
||||||
|
await this.secureStorageService.get(SecureStorageKeys[key])
|
||||||
|
);
|
||||||
|
await this.secureStorageService.remove(SecureStorageKeys[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,208 +1,252 @@
|
|||||||
import { DirectoryType } from '../enums/directoryType';
|
import { DirectoryType } from "../enums/directoryType";
|
||||||
|
|
||||||
import { GroupEntry } from '../models/groupEntry';
|
import { GroupEntry } from "../models/groupEntry";
|
||||||
import { SyncConfiguration } from '../models/syncConfiguration';
|
import { SyncConfiguration } from "../models/syncConfiguration";
|
||||||
import { UserEntry } from '../models/userEntry';
|
import { UserEntry } from "../models/userEntry";
|
||||||
|
|
||||||
import { OrganizationImportRequest } from 'jslib-common/models/request/organizationImportRequest';
|
import { OrganizationImportRequest } from "jslib-common/models/request/organizationImportRequest";
|
||||||
|
|
||||||
import { ApiService } from 'jslib-common/abstractions/api.service';
|
import { ApiService } from "jslib-common/abstractions/api.service";
|
||||||
import { CryptoFunctionService } from 'jslib-common/abstractions/cryptoFunction.service';
|
import { CryptoFunctionService } from "jslib-common/abstractions/cryptoFunction.service";
|
||||||
import { EnvironmentService } from 'jslib-common/abstractions/environment.service';
|
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
|
||||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
import { LogService } from 'jslib-common/abstractions/log.service';
|
import { LogService } from "jslib-common/abstractions/log.service";
|
||||||
import { MessagingService } from 'jslib-common/abstractions/messaging.service';
|
import { MessagingService } from "jslib-common/abstractions/messaging.service";
|
||||||
|
|
||||||
import { Utils } from 'jslib-common/misc/utils';
|
import { Utils } from "jslib-common/misc/utils";
|
||||||
|
|
||||||
import { AzureDirectoryService } from './azure-directory.service';
|
import { StateService } from "../abstractions/state.service";
|
||||||
import { ConfigurationService } from './configuration.service';
|
import { AzureDirectoryService } from "./azure-directory.service";
|
||||||
import { IDirectoryService } from './directory.service';
|
import { IDirectoryService } from "./directory.service";
|
||||||
import { GSuiteDirectoryService } from './gsuite-directory.service';
|
import { GSuiteDirectoryService } from "./gsuite-directory.service";
|
||||||
import { LdapDirectoryService } from './ldap-directory.service';
|
import { LdapDirectoryService } from "./ldap-directory.service";
|
||||||
import { OktaDirectoryService } from './okta-directory.service';
|
import { OktaDirectoryService } from "./okta-directory.service";
|
||||||
import { OneLoginDirectoryService } from './onelogin-directory.service';
|
import { OneLoginDirectoryService } from "./onelogin-directory.service";
|
||||||
|
|
||||||
export class SyncService {
|
export class SyncService {
|
||||||
private dirType: DirectoryType;
|
private dirType: DirectoryType;
|
||||||
|
|
||||||
constructor(private configurationService: ConfigurationService, private logService: LogService,
|
constructor(
|
||||||
private cryptoFunctionService: CryptoFunctionService, private apiService: ApiService,
|
private logService: LogService,
|
||||||
private messagingService: MessagingService, private i18nService: I18nService,
|
private cryptoFunctionService: CryptoFunctionService,
|
||||||
private environmentService: EnvironmentService) { }
|
private apiService: ApiService,
|
||||||
|
private messagingService: MessagingService,
|
||||||
|
private i18nService: I18nService,
|
||||||
|
private environmentService: EnvironmentService,
|
||||||
|
private stateService: StateService
|
||||||
|
) {}
|
||||||
|
|
||||||
async sync(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> {
|
async sync(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> {
|
||||||
this.dirType = await this.configurationService.getDirectoryType();
|
this.dirType = await this.stateService.getDirectoryType();
|
||||||
if (this.dirType == null) {
|
if (this.dirType == null) {
|
||||||
throw new Error('No directory configured.');
|
throw new Error("No directory configured.");
|
||||||
}
|
|
||||||
|
|
||||||
const directoryService = this.getDirectoryService();
|
|
||||||
if (directoryService == null) {
|
|
||||||
throw new Error('Cannot load directory service.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const syncConfig = await this.configurationService.getSync();
|
|
||||||
const startingGroupDelta = await this.configurationService.getGroupDeltaToken();
|
|
||||||
const startingUserDelta = await this.configurationService.getUserDeltaToken();
|
|
||||||
const now = new Date();
|
|
||||||
|
|
||||||
this.messagingService.send('dirSyncStarted');
|
|
||||||
try {
|
|
||||||
const entries = await directoryService.getEntries(force || syncConfig.overwriteExisting, test);
|
|
||||||
let groups = entries[0];
|
|
||||||
let users = this.filterUnsupportedUsers(entries[1]);
|
|
||||||
|
|
||||||
if (groups != null && groups.length > 0) {
|
|
||||||
this.flattenUsersToGroups(groups, groups);
|
|
||||||
}
|
|
||||||
|
|
||||||
users = this.removeDuplicateUsers(users);
|
|
||||||
|
|
||||||
if (test || (!syncConfig.overwriteExisting &&
|
|
||||||
(groups == null || groups.length === 0) && (users == null || users.length === 0))) {
|
|
||||||
if (!test) {
|
|
||||||
await this.saveSyncTimes(syncConfig, now);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.messagingService.send('dirSyncCompleted', { successfully: true });
|
|
||||||
return [groups, users];
|
|
||||||
}
|
|
||||||
|
|
||||||
const req = this.buildRequest(groups, users, syncConfig.removeDisabled, syncConfig.overwriteExisting, syncConfig.largeImport);
|
|
||||||
const reqJson = JSON.stringify(req);
|
|
||||||
|
|
||||||
const orgId = await this.configurationService.getOrganizationId();
|
|
||||||
if (orgId == null) {
|
|
||||||
throw new Error('Organization not set.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Remove hashLegacy once we're sure clients have had time to sync new hashes
|
|
||||||
let hashLegacy: string = null;
|
|
||||||
const hashBuffLegacy = await this.cryptoFunctionService.hash(this.environmentService.getApiUrl() + reqJson, 'sha256');
|
|
||||||
if (hashBuffLegacy != null) {
|
|
||||||
hashLegacy = Utils.fromBufferToB64(hashBuffLegacy);
|
|
||||||
}
|
|
||||||
let hash: string = null;
|
|
||||||
const hashBuff = await this.cryptoFunctionService.hash(this.environmentService.getApiUrl() + orgId + reqJson, 'sha256');
|
|
||||||
if (hashBuff != null) {
|
|
||||||
hash = Utils.fromBufferToB64(hashBuff);
|
|
||||||
}
|
|
||||||
const lastHash = await this.configurationService.getLastSyncHash();
|
|
||||||
|
|
||||||
if (lastHash == null || (hash !== lastHash && hashLegacy !== lastHash)) {
|
|
||||||
await this.apiService.postPublicImportDirectory(req);
|
|
||||||
await this.configurationService.saveLastSyncHash(hash);
|
|
||||||
} else {
|
|
||||||
groups = null;
|
|
||||||
users = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.saveSyncTimes(syncConfig, now);
|
|
||||||
this.messagingService.send('dirSyncCompleted', { successfully: true });
|
|
||||||
return [groups, users];
|
|
||||||
} catch (e) {
|
|
||||||
if (!test) {
|
|
||||||
await this.configurationService.saveGroupDeltaToken(startingGroupDelta);
|
|
||||||
await this.configurationService.saveUserDeltaToken(startingUserDelta);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.messagingService.send('dirSyncCompleted', { successfully: false });
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private removeDuplicateUsers(users: UserEntry[]) {
|
const directoryService = this.getDirectoryService();
|
||||||
const uniqueUsers = new Array<UserEntry>();
|
if (directoryService == null) {
|
||||||
const processedUsers = new Map<string, string>();
|
throw new Error("Cannot load directory service.");
|
||||||
const duplicateEmails = new Array<string>();
|
|
||||||
|
|
||||||
// UserEntrys with the same email are ignored if their properties are the same
|
|
||||||
// UserEntrys with the same email but different properties will throw an error
|
|
||||||
users.forEach(u => {
|
|
||||||
if (processedUsers.has(u.email)) {
|
|
||||||
if (processedUsers.get(u.email) != JSON.stringify(u)) {
|
|
||||||
duplicateEmails.push(u.email);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
uniqueUsers.push(u);
|
|
||||||
processedUsers.set(u.email, JSON.stringify(u));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (duplicateEmails.length > 0) {
|
|
||||||
const emailsMessage = duplicateEmails.length < 4 ?
|
|
||||||
duplicateEmails.join('\n') :
|
|
||||||
duplicateEmails.slice(0, 3).join('\n') + '\n' + this.i18nService.t('andMore', `${duplicateEmails.length - 3}`);
|
|
||||||
throw new Error(this.i18nService.t('duplicateEmails') + '\n' + emailsMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
return uniqueUsers;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private filterUnsupportedUsers(users: UserEntry[]): UserEntry[] {
|
const syncConfig = await this.stateService.getSync();
|
||||||
return users == null ? null : users.filter(u => u.email?.length <= 256);
|
const startingGroupDelta = await this.stateService.getGroupDelta();
|
||||||
|
const startingUserDelta = await this.stateService.getUserDelta();
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
this.messagingService.send("dirSyncStarted");
|
||||||
|
try {
|
||||||
|
const entries = await directoryService.getEntries(
|
||||||
|
force || syncConfig.overwriteExisting,
|
||||||
|
test
|
||||||
|
);
|
||||||
|
let groups = entries[0];
|
||||||
|
let users = this.filterUnsupportedUsers(entries[1]);
|
||||||
|
|
||||||
|
if (groups != null && groups.length > 0) {
|
||||||
|
this.flattenUsersToGroups(groups, groups);
|
||||||
|
}
|
||||||
|
|
||||||
|
users = this.removeDuplicateUsers(users);
|
||||||
|
|
||||||
|
if (
|
||||||
|
test ||
|
||||||
|
(!syncConfig.overwriteExisting &&
|
||||||
|
(groups == null || groups.length === 0) &&
|
||||||
|
(users == null || users.length === 0))
|
||||||
|
) {
|
||||||
|
if (!test) {
|
||||||
|
await this.saveSyncTimes(syncConfig, now);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.messagingService.send("dirSyncCompleted", { successfully: true });
|
||||||
|
return [groups, users];
|
||||||
|
}
|
||||||
|
|
||||||
|
const req = this.buildRequest(
|
||||||
|
groups,
|
||||||
|
users,
|
||||||
|
syncConfig.removeDisabled,
|
||||||
|
syncConfig.overwriteExisting,
|
||||||
|
syncConfig.largeImport
|
||||||
|
);
|
||||||
|
const reqJson = JSON.stringify(req);
|
||||||
|
|
||||||
|
const orgId = await this.stateService.getOrganizationId();
|
||||||
|
if (orgId == null) {
|
||||||
|
throw new Error("Organization not set.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Remove hashLegacy once we're sure clients have had time to sync new hashes
|
||||||
|
let hashLegacy: string = null;
|
||||||
|
const hashBuffLegacy = await this.cryptoFunctionService.hash(
|
||||||
|
this.environmentService.getApiUrl() + reqJson,
|
||||||
|
"sha256"
|
||||||
|
);
|
||||||
|
if (hashBuffLegacy != null) {
|
||||||
|
hashLegacy = Utils.fromBufferToB64(hashBuffLegacy);
|
||||||
|
}
|
||||||
|
let hash: string = null;
|
||||||
|
const hashBuff = await this.cryptoFunctionService.hash(
|
||||||
|
this.environmentService.getApiUrl() + orgId + reqJson,
|
||||||
|
"sha256"
|
||||||
|
);
|
||||||
|
if (hashBuff != null) {
|
||||||
|
hash = Utils.fromBufferToB64(hashBuff);
|
||||||
|
}
|
||||||
|
const lastHash = await this.stateService.getLastSyncHash();
|
||||||
|
|
||||||
|
if (lastHash == null || (hash !== lastHash && hashLegacy !== lastHash)) {
|
||||||
|
await this.apiService.postPublicImportDirectory(req);
|
||||||
|
await this.stateService.setLastSyncHash(hash);
|
||||||
|
} else {
|
||||||
|
groups = null;
|
||||||
|
users = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.saveSyncTimes(syncConfig, now);
|
||||||
|
this.messagingService.send("dirSyncCompleted", { successfully: true });
|
||||||
|
return [groups, users];
|
||||||
|
} catch (e) {
|
||||||
|
if (!test) {
|
||||||
|
await this.stateService.setGroupDelta(startingGroupDelta);
|
||||||
|
await this.stateService.setUserDelta(startingUserDelta);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.messagingService.send("dirSyncCompleted", { successfully: false });
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeDuplicateUsers(users: UserEntry[]) {
|
||||||
|
const uniqueUsers = new Array<UserEntry>();
|
||||||
|
const processedActiveUsers = new Map<string, string>();
|
||||||
|
const processedDeletedUsers = new Map<string, string>();
|
||||||
|
const duplicateEmails = new Array<string>();
|
||||||
|
|
||||||
|
// UserEntrys with the same email are ignored if their properties are the same
|
||||||
|
// UserEntrys with the same email but different properties will throw an error, unless they are all in a deleted state.
|
||||||
|
users.forEach((u) => {
|
||||||
|
if (processedActiveUsers.has(u.email)) {
|
||||||
|
if (processedActiveUsers.get(u.email) !== JSON.stringify(u)) {
|
||||||
|
duplicateEmails.push(u.email);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!u.deleted) {
|
||||||
|
// Check that active UserEntry does not conflict with a deleted UserEntry
|
||||||
|
if (processedDeletedUsers.has(u.email)) {
|
||||||
|
duplicateEmails.push(u.email);
|
||||||
|
} else {
|
||||||
|
processedActiveUsers.set(u.email, JSON.stringify(u));
|
||||||
|
uniqueUsers.push(u);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// UserEntrys with duplicate email will not throw an error if they are all deleted. They will be synced.
|
||||||
|
processedDeletedUsers.set(u.email, JSON.stringify(u));
|
||||||
|
uniqueUsers.push(u);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (duplicateEmails.length > 0) {
|
||||||
|
const emailsMessage =
|
||||||
|
duplicateEmails.length < 4
|
||||||
|
? duplicateEmails.join("\n")
|
||||||
|
: duplicateEmails.slice(0, 3).join("\n") +
|
||||||
|
"\n" +
|
||||||
|
this.i18nService.t("andMore", `${duplicateEmails.length - 3}`);
|
||||||
|
throw new Error(this.i18nService.t("duplicateEmails") + "\n" + emailsMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
private flattenUsersToGroups(levelGroups: GroupEntry[], allGroups: GroupEntry[]): Set<string> {
|
return uniqueUsers;
|
||||||
let allUsers = new Set<string>();
|
}
|
||||||
if (allGroups == null) {
|
|
||||||
return allUsers;
|
|
||||||
}
|
|
||||||
for (const group of levelGroups) {
|
|
||||||
const childGroups = allGroups.filter(g => group.groupMemberReferenceIds.has(g.referenceId));
|
|
||||||
const childUsers = this.flattenUsersToGroups(childGroups, allGroups);
|
|
||||||
childUsers.forEach(id => group.userMemberExternalIds.add(id));
|
|
||||||
allUsers = new Set([...allUsers, ...group.userMemberExternalIds]);
|
|
||||||
}
|
|
||||||
return allUsers;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getDirectoryService(): IDirectoryService {
|
private filterUnsupportedUsers(users: UserEntry[]): UserEntry[] {
|
||||||
switch (this.dirType) {
|
return users == null ? null : users.filter((u) => u.email?.length <= 256);
|
||||||
case DirectoryType.GSuite:
|
}
|
||||||
return new GSuiteDirectoryService(this.configurationService, this.logService, this.i18nService);
|
|
||||||
case DirectoryType.AzureActiveDirectory:
|
|
||||||
return new AzureDirectoryService(this.configurationService, this.logService, this.i18nService);
|
|
||||||
case DirectoryType.Ldap:
|
|
||||||
return new LdapDirectoryService(this.configurationService, this.logService, this.i18nService);
|
|
||||||
case DirectoryType.Okta:
|
|
||||||
return new OktaDirectoryService(this.configurationService, this.logService, this.i18nService);
|
|
||||||
case DirectoryType.OneLogin:
|
|
||||||
return new OneLoginDirectoryService(this.configurationService, this.logService, this.i18nService);
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private buildRequest(groups: GroupEntry[], users: UserEntry[], removeDisabled: boolean, overwriteExisting: boolean,
|
private flattenUsersToGroups(levelGroups: GroupEntry[], allGroups: GroupEntry[]): Set<string> {
|
||||||
largeImport: boolean = false) {
|
let allUsers = new Set<string>();
|
||||||
return new OrganizationImportRequest({
|
if (allGroups == null) {
|
||||||
groups: (groups ?? []).map(g => {
|
return allUsers;
|
||||||
return {
|
|
||||||
name: g.name,
|
|
||||||
externalId: g.externalId,
|
|
||||||
memberExternalIds: Array.from(g.userMemberExternalIds),
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
users: (users ?? []).map(u => {
|
|
||||||
return {
|
|
||||||
email: u.email,
|
|
||||||
externalId: u.externalId,
|
|
||||||
deleted: u.deleted || (removeDisabled && u.disabled),
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
overwriteExisting: overwriteExisting,
|
|
||||||
largeImport: largeImport,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
for (const group of levelGroups) {
|
||||||
|
const childGroups = allGroups.filter((g) => group.groupMemberReferenceIds.has(g.referenceId));
|
||||||
|
const childUsers = this.flattenUsersToGroups(childGroups, allGroups);
|
||||||
|
childUsers.forEach((id) => group.userMemberExternalIds.add(id));
|
||||||
|
allUsers = new Set([...allUsers, ...group.userMemberExternalIds]);
|
||||||
|
}
|
||||||
|
return allUsers;
|
||||||
|
}
|
||||||
|
|
||||||
private async saveSyncTimes(syncConfig: SyncConfiguration, time: Date) {
|
private getDirectoryService(): IDirectoryService {
|
||||||
if (syncConfig.groups) {
|
switch (this.dirType) {
|
||||||
await this.configurationService.saveLastGroupSyncDate(time);
|
case DirectoryType.GSuite:
|
||||||
}
|
return new GSuiteDirectoryService(this.logService, this.i18nService, this.stateService);
|
||||||
if (syncConfig.users) {
|
case DirectoryType.AzureActiveDirectory:
|
||||||
await this.configurationService.saveLastUserSyncDate(time);
|
return new AzureDirectoryService(this.logService, this.i18nService, this.stateService);
|
||||||
}
|
case DirectoryType.Ldap:
|
||||||
|
return new LdapDirectoryService(this.logService, this.i18nService, this.stateService);
|
||||||
|
case DirectoryType.Okta:
|
||||||
|
return new OktaDirectoryService(this.logService, this.i18nService, this.stateService);
|
||||||
|
case DirectoryType.OneLogin:
|
||||||
|
return new OneLoginDirectoryService(this.logService, this.i18nService, this.stateService);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildRequest(
|
||||||
|
groups: GroupEntry[],
|
||||||
|
users: UserEntry[],
|
||||||
|
removeDisabled: boolean,
|
||||||
|
overwriteExisting: boolean,
|
||||||
|
largeImport: boolean = false
|
||||||
|
) {
|
||||||
|
return new OrganizationImportRequest({
|
||||||
|
groups: (groups ?? []).map((g) => {
|
||||||
|
return {
|
||||||
|
name: g.name,
|
||||||
|
externalId: g.externalId,
|
||||||
|
memberExternalIds: Array.from(g.userMemberExternalIds),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
users: (users ?? []).map((u) => {
|
||||||
|
return {
|
||||||
|
email: u.email,
|
||||||
|
externalId: u.externalId,
|
||||||
|
deleted: u.deleted || (removeDisabled && u.disabled),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
overwriteExisting: overwriteExisting,
|
||||||
|
largeImport: largeImport,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async saveSyncTimes(syncConfig: SyncConfiguration, time: Date) {
|
||||||
|
if (syncConfig.groups) {
|
||||||
|
await this.stateService.setLastGroupSync(time);
|
||||||
|
}
|
||||||
|
if (syncConfig.users) {
|
||||||
|
await this.stateService.setLastUserSync(time);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
179
src/utils.ts
179
src/utils.ts
@@ -1,100 +1,105 @@
|
|||||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
|
|
||||||
import { SyncService } from './services/sync.service';
|
import { SyncService } from "./services/sync.service";
|
||||||
|
|
||||||
import { Entry } from './models/entry';
|
import { Entry } from "./models/entry";
|
||||||
import { LdapConfiguration } from './models/ldapConfiguration';
|
import { LdapConfiguration } from "./models/ldapConfiguration";
|
||||||
import { SimResult } from './models/simResult';
|
import { SimResult } from "./models/simResult";
|
||||||
import { SyncConfiguration } from './models/syncConfiguration';
|
import { SyncConfiguration } from "./models/syncConfiguration";
|
||||||
import { UserEntry } from './models/userEntry';
|
import { UserEntry } from "./models/userEntry";
|
||||||
|
|
||||||
export class ConnectorUtils {
|
export class ConnectorUtils {
|
||||||
static async simulate(syncService: SyncService, i18nService: I18nService, sinceLast: boolean): Promise<SimResult> {
|
static async simulate(
|
||||||
return new Promise(async (resolve, reject) => {
|
syncService: SyncService,
|
||||||
const simResult = new SimResult();
|
i18nService: I18nService,
|
||||||
try {
|
sinceLast: boolean
|
||||||
const result = await syncService.sync(!sinceLast, true);
|
): Promise<SimResult> {
|
||||||
if (result[0] != null) {
|
return new Promise(async (resolve, reject) => {
|
||||||
simResult.groups = result[0];
|
const simResult = new SimResult();
|
||||||
}
|
try {
|
||||||
if (result[1] != null) {
|
const result = await syncService.sync(!sinceLast, true);
|
||||||
simResult.users = result[1];
|
if (result[0] != null) {
|
||||||
}
|
simResult.groups = result[0];
|
||||||
} catch (e) {
|
}
|
||||||
simResult.groups = null;
|
if (result[1] != null) {
|
||||||
simResult.users = null;
|
simResult.users = result[1];
|
||||||
reject(e || i18nService.t('syncError'));
|
}
|
||||||
return;
|
} catch (e) {
|
||||||
}
|
simResult.groups = null;
|
||||||
|
simResult.users = null;
|
||||||
|
reject(e || i18nService.t("syncError"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const userMap = new Map<string, UserEntry>();
|
const userMap = new Map<string, UserEntry>();
|
||||||
this.sortEntries(simResult.users, i18nService);
|
this.sortEntries(simResult.users, i18nService);
|
||||||
for (const u of simResult.users) {
|
for (const u of simResult.users) {
|
||||||
userMap.set(u.externalId, u);
|
userMap.set(u.externalId, u);
|
||||||
if (u.deleted) {
|
if (u.deleted) {
|
||||||
simResult.deletedUsers.push(u);
|
simResult.deletedUsers.push(u);
|
||||||
} else if (u.disabled) {
|
} else if (u.disabled) {
|
||||||
simResult.disabledUsers.push(u);
|
simResult.disabledUsers.push(u);
|
||||||
} else {
|
} else {
|
||||||
simResult.enabledUsers.push(u);
|
simResult.enabledUsers.push(u);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.sortEntries(simResult.groups, i18nService);
|
this.sortEntries(simResult.groups, i18nService);
|
||||||
for (const g of simResult.groups) {
|
for (const g of simResult.groups) {
|
||||||
if (g.userMemberExternalIds == null) {
|
if (g.userMemberExternalIds == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
|
||||||
|
|
||||||
const anyG = (g as any);
|
|
||||||
anyG.users = [];
|
|
||||||
for (const uid of g.userMemberExternalIds) {
|
|
||||||
if (userMap.has(uid)) {
|
|
||||||
anyG.users.push(userMap.get(uid));
|
|
||||||
} else {
|
|
||||||
anyG.users.push({ displayName: uid });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.sortEntries(anyG.users, i18nService);
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve(simResult);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static adjustConfigForSave(ldap: LdapConfiguration, sync: SyncConfiguration) {
|
|
||||||
if (ldap.ad) {
|
|
||||||
sync.creationDateAttribute = 'whenCreated';
|
|
||||||
sync.revisionDateAttribute = 'whenChanged';
|
|
||||||
sync.emailPrefixAttribute = 'sAMAccountName';
|
|
||||||
sync.memberAttribute = 'member';
|
|
||||||
sync.userObjectClass = 'person';
|
|
||||||
sync.groupObjectClass = 'group';
|
|
||||||
sync.userEmailAttribute = 'mail';
|
|
||||||
sync.groupNameAttribute = 'name';
|
|
||||||
|
|
||||||
if (sync.groupPath == null) {
|
|
||||||
sync.groupPath = 'CN=Users';
|
|
||||||
}
|
|
||||||
if (sync.userPath == null) {
|
|
||||||
sync.userPath = 'CN=Users';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sync.interval != null) {
|
const anyG = g as any;
|
||||||
if (sync.interval <= 0) {
|
anyG.users = [];
|
||||||
sync.interval = null;
|
for (const uid of g.userMemberExternalIds) {
|
||||||
} else if (sync.interval < 5) {
|
if (userMap.has(uid)) {
|
||||||
sync.interval = 5;
|
anyG.users.push(userMap.get(uid));
|
||||||
}
|
} else {
|
||||||
|
anyG.users.push({ displayName: uid });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.sortEntries(anyG.users, i18nService);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(simResult);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static adjustConfigForSave(ldap: LdapConfiguration, sync: SyncConfiguration) {
|
||||||
|
if (ldap.ad) {
|
||||||
|
sync.creationDateAttribute = "whenCreated";
|
||||||
|
sync.revisionDateAttribute = "whenChanged";
|
||||||
|
sync.emailPrefixAttribute = "sAMAccountName";
|
||||||
|
sync.memberAttribute = "member";
|
||||||
|
sync.userObjectClass = "person";
|
||||||
|
sync.groupObjectClass = "group";
|
||||||
|
sync.userEmailAttribute = "mail";
|
||||||
|
sync.groupNameAttribute = "name";
|
||||||
|
|
||||||
|
if (sync.groupPath == null) {
|
||||||
|
sync.groupPath = "CN=Users";
|
||||||
|
}
|
||||||
|
if (sync.userPath == null) {
|
||||||
|
sync.userPath = "CN=Users";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static sortEntries(arr: Entry[], i18nService: I18nService) {
|
if (sync.interval != null) {
|
||||||
arr.sort((a, b) => {
|
if (sync.interval <= 0) {
|
||||||
return i18nService.collator ? i18nService.collator.compare(a.displayName, b.displayName) :
|
sync.interval = null;
|
||||||
a.displayName.localeCompare(b.displayName);
|
} else if (sync.interval < 5) {
|
||||||
});
|
sync.interval = 5;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static sortEntries(arr: Entry[], i18nService: I18nService) {
|
||||||
|
arr.sort((a, b) => {
|
||||||
|
return i18nService.collator
|
||||||
|
? i18nService.collator.compare(a.displayName, b.displayName)
|
||||||
|
: a.displayName.localeCompare(b.displayName);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,28 +12,15 @@
|
|||||||
"types": [],
|
"types": [],
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"tldjs": [
|
"tldjs": ["jslib/src/misc/tldjs.noop"],
|
||||||
"jslib/src/misc/tldjs.noop"
|
"jslib-common/*": ["jslib/common/src/*"],
|
||||||
],
|
"jslib-angular/*": ["jslib/angular/src/*"],
|
||||||
"jslib-common/*": [
|
"jslib-electron/*": ["jslib/electron/src/*"],
|
||||||
"jslib/common/src/*"
|
"jslib-node/*": ["jslib/node/src/*"]
|
||||||
],
|
|
||||||
"jslib-angular/*": [
|
|
||||||
"jslib/angular/src/*"
|
|
||||||
],
|
|
||||||
"jslib-electron/*": [
|
|
||||||
"jslib/electron/src/*"
|
|
||||||
],
|
|
||||||
"jslib-node/*": [
|
|
||||||
"jslib/node/src/*"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"angularCompilerOptions": {
|
"angularCompilerOptions": {
|
||||||
"preserveWhitespaces": true
|
"preserveWhitespaces": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["src", "src-cli"]
|
||||||
"src",
|
|
||||||
"src-cli"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
28
tslint.json
28
tslint.json
@@ -1,17 +1,17 @@
|
|||||||
{
|
{
|
||||||
"extends": "tslint:recommended",
|
"extends": "tslint:recommended",
|
||||||
"rules": {
|
"rules": {
|
||||||
"align": [ true, "statements", "members" ],
|
"align": [true, "statements", "members"],
|
||||||
"ban-types": {
|
"ban-types": {
|
||||||
"options": [
|
"options": [
|
||||||
[ "Object", "Avoid using the `Object` type. Did you mean `object`?" ],
|
["Object", "Avoid using the `Object` type. Did you mean `object`?"],
|
||||||
[ "Boolean", "Avoid using the `Boolean` type. Did you mean `boolean`?" ],
|
["Boolean", "Avoid using the `Boolean` type. Did you mean `boolean`?"],
|
||||||
[ "Number", "Avoid using the `Number` type. Did you mean `number`?" ],
|
["Number", "Avoid using the `Number` type. Did you mean `number`?"],
|
||||||
[ "String", "Avoid using the `String` type. Did you mean `string`?" ],
|
["String", "Avoid using the `String` type. Did you mean `string`?"],
|
||||||
[ "Symbol", "Avoid using the `Symbol` type. Did you mean `symbol`?" ]
|
["Symbol", "Avoid using the `Symbol` type. Did you mean `symbol`?"]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"member-access": [ true, "no-public" ],
|
"member-access": [true, "no-public"],
|
||||||
"member-ordering": [
|
"member-ordering": [
|
||||||
true,
|
true,
|
||||||
{
|
{
|
||||||
@@ -34,11 +34,10 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"no-empty": [ true, "allow-empty-catch" ],
|
"no-empty": [true],
|
||||||
"object-literal-sort-keys": false,
|
"object-literal-sort-keys": false,
|
||||||
"object-literal-shorthand": [ true, "never" ],
|
"object-literal-shorthand": [true, "never"],
|
||||||
"prefer-for-of": false,
|
"prefer-for-of": false,
|
||||||
"quotemark": [ true, "single" ],
|
|
||||||
"whitespace": [
|
"whitespace": [
|
||||||
true,
|
true,
|
||||||
"check-branch",
|
"check-branch",
|
||||||
@@ -51,14 +50,7 @@
|
|||||||
],
|
],
|
||||||
"max-classes-per-file": false,
|
"max-classes-per-file": false,
|
||||||
"ordered-imports": true,
|
"ordered-imports": true,
|
||||||
"arrow-parens": [
|
"arrow-parens": [true],
|
||||||
true,
|
|
||||||
"ban-single-arg-parens"
|
|
||||||
],
|
|
||||||
"semicolon": [
|
|
||||||
true,
|
|
||||||
"always"
|
|
||||||
],
|
|
||||||
"trailing-comma": [
|
"trailing-comma": [
|
||||||
true,
|
true,
|
||||||
{
|
{
|
||||||
|
|||||||
125
webpack.cli.js
125
webpack.cli.js
@@ -1,76 +1,77 @@
|
|||||||
const path = require('path');
|
const path = require("path");
|
||||||
const webpack = require('webpack');
|
const webpack = require("webpack");
|
||||||
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
|
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
|
||||||
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
||||||
const nodeExternals = require('webpack-node-externals');
|
const nodeExternals = require("webpack-node-externals");
|
||||||
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
|
const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
|
||||||
|
|
||||||
if (process.env.NODE_ENV == null) {
|
if (process.env.NODE_ENV == null) {
|
||||||
process.env.NODE_ENV = 'development';
|
process.env.NODE_ENV = "development";
|
||||||
}
|
}
|
||||||
const ENV = process.env.ENV = process.env.NODE_ENV;
|
const ENV = (process.env.ENV = process.env.NODE_ENV);
|
||||||
|
|
||||||
const moduleRules = [
|
const moduleRules = [
|
||||||
{
|
{
|
||||||
test: /\.ts$/,
|
test: /\.ts$/,
|
||||||
enforce: 'pre',
|
enforce: "pre",
|
||||||
loader: 'tslint-loader',
|
loader: "tslint-loader",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.ts$/,
|
test: /\.ts$/,
|
||||||
loaders: ['ts-loader'],
|
use: "ts-loader",
|
||||||
exclude: path.resolve(__dirname, 'node_modules'),
|
exclude: path.resolve(__dirname, "node_modules"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.node$/,
|
test: /\.node$/,
|
||||||
loader: 'node-loader',
|
loader: "node-loader",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const plugins = [
|
const plugins = [
|
||||||
new CleanWebpackPlugin(),
|
new CleanWebpackPlugin(),
|
||||||
new CopyWebpackPlugin({
|
new CopyWebpackPlugin({
|
||||||
patterns: [
|
patterns: [{ from: "./src/locales", to: "locales" }],
|
||||||
{ from: './src/locales', to: 'locales' },
|
}),
|
||||||
],
|
new webpack.DefinePlugin({
|
||||||
}),
|
"process.env.BWCLI_ENV": JSON.stringify(ENV),
|
||||||
new webpack.DefinePlugin({
|
}),
|
||||||
'process.env.BWCLI_ENV': JSON.stringify(ENV),
|
new webpack.BannerPlugin({
|
||||||
}),
|
banner: "#!/usr/bin/env node",
|
||||||
new webpack.BannerPlugin({
|
raw: true,
|
||||||
banner: '#!/usr/bin/env node',
|
}),
|
||||||
raw: true
|
new webpack.IgnorePlugin({
|
||||||
}),
|
resourceRegExp: /^encoding$/,
|
||||||
new webpack.IgnorePlugin(/^encoding$/, /node-fetch/),
|
contextRegExp: /node-fetch/,
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
mode: ENV,
|
mode: ENV,
|
||||||
target: 'node',
|
target: "node",
|
||||||
devtool: ENV === 'development' ? 'eval-source-map' : 'source-map',
|
devtool: ENV === "development" ? "eval-source-map" : "source-map",
|
||||||
node: {
|
node: {
|
||||||
__dirname: false,
|
__dirname: false,
|
||||||
__filename: false,
|
__filename: false,
|
||||||
},
|
},
|
||||||
entry: {
|
entry: {
|
||||||
'bwdc': './src/bwdc.ts',
|
bwdc: "./src/bwdc.ts",
|
||||||
},
|
},
|
||||||
optimization: {
|
optimization: {
|
||||||
minimize: false,
|
minimize: false,
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
extensions: ['.ts', '.js', '.json'],
|
extensions: [".ts", ".js", ".json"],
|
||||||
plugins: [new TsconfigPathsPlugin({ configFile: './tsconfig.json' })],
|
plugins: [new TsconfigPathsPlugin({ configFile: "./tsconfig.json" })],
|
||||||
symlinks: false,
|
symlinks: false,
|
||||||
modules: [path.resolve('node_modules')],
|
modules: [path.resolve("node_modules")],
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
filename: '[name].js',
|
filename: "[name].js",
|
||||||
path: path.resolve(__dirname, 'build-cli'),
|
path: path.resolve(__dirname, "build-cli"),
|
||||||
},
|
},
|
||||||
module: { rules: moduleRules },
|
module: { rules: moduleRules },
|
||||||
plugins: plugins,
|
plugins: plugins,
|
||||||
externals: [nodeExternals()],
|
externals: [nodeExternals()],
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = config;
|
module.exports = config;
|
||||||
|
|||||||
118
webpack.main.js
118
webpack.main.js
@@ -1,68 +1,68 @@
|
|||||||
const path = require('path');
|
const path = require("path");
|
||||||
const { merge } = require('webpack-merge');
|
const { merge } = require("webpack-merge");
|
||||||
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
||||||
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
|
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
|
||||||
const nodeExternals = require('webpack-node-externals');
|
const nodeExternals = require("webpack-node-externals");
|
||||||
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
|
const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
|
||||||
|
|
||||||
const common = {
|
const common = {
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
test: /\.ts$/,
|
test: /\.ts$/,
|
||||||
enforce: 'pre',
|
enforce: "pre",
|
||||||
loader: 'tslint-loader',
|
loader: "tslint-loader",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.tsx?$/,
|
test: /\.tsx?$/,
|
||||||
use: 'ts-loader',
|
use: "ts-loader",
|
||||||
exclude: /node_modules\/(?!(@bitwarden)\/).*/,
|
exclude: /node_modules\/(?!(@bitwarden)\/).*/,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [],
|
||||||
resolve: {
|
resolve: {
|
||||||
extensions: ['.tsx', '.ts', '.js'],
|
extensions: [".tsx", ".ts", ".js"],
|
||||||
plugins: [new TsconfigPathsPlugin({ configFile: './tsconfig.json' })],
|
plugins: [new TsconfigPathsPlugin({ configFile: "./tsconfig.json" })],
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
filename: '[name].js',
|
filename: "[name].js",
|
||||||
path: path.resolve(__dirname, 'build'),
|
path: path.resolve(__dirname, "build"),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const main = {
|
const main = {
|
||||||
mode: 'production',
|
mode: "production",
|
||||||
target: 'electron-main',
|
target: "electron-main",
|
||||||
node: {
|
node: {
|
||||||
__dirname: false,
|
__dirname: false,
|
||||||
__filename: false,
|
__filename: false,
|
||||||
},
|
},
|
||||||
entry: {
|
entry: {
|
||||||
'main': './src/main.ts',
|
main: "./src/main.ts",
|
||||||
},
|
},
|
||||||
optimization: {
|
optimization: {
|
||||||
minimize: false,
|
minimize: false,
|
||||||
},
|
},
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
test: /\.node$/,
|
test: /\.node$/,
|
||||||
loader: 'node-loader',
|
loader: "node-loader",
|
||||||
},
|
},
|
||||||
],
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
new CleanWebpackPlugin(),
|
|
||||||
new CopyWebpackPlugin({
|
|
||||||
patterns: [
|
|
||||||
'./src/package.json',
|
|
||||||
{ from: './src/images', to: 'images' },
|
|
||||||
{ from: './src/locales', to: 'locales' },
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
],
|
],
|
||||||
externals: [nodeExternals()],
|
},
|
||||||
|
plugins: [
|
||||||
|
new CleanWebpackPlugin(),
|
||||||
|
new CopyWebpackPlugin({
|
||||||
|
patterns: [
|
||||||
|
"./src/package.json",
|
||||||
|
{ from: "./src/images", to: "images" },
|
||||||
|
{ from: "./src/locales", to: "locales" },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
externals: [nodeExternals()],
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = merge(common, main);
|
module.exports = merge(common, main);
|
||||||
|
|||||||
@@ -1,133 +1,129 @@
|
|||||||
const path = require('path');
|
const path = require("path");
|
||||||
const webpack = require('webpack');
|
const webpack = require("webpack");
|
||||||
const { merge } = require('webpack-merge');
|
const { merge } = require("webpack-merge");
|
||||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
||||||
const AngularCompilerPlugin = require('@ngtools/webpack').AngularCompilerPlugin;
|
const { AngularWebpackPlugin } = require("@ngtools/webpack");
|
||||||
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
|
const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
|
||||||
|
|
||||||
const common = {
|
const common = {
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
test: /\.ts$/,
|
test: /\.ts$/,
|
||||||
enforce: 'pre',
|
enforce: "pre",
|
||||||
loader: 'tslint-loader',
|
loader: "tslint-loader",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /(?:\.ngfactory\.js|\.ngstyle\.js|\.ts)$/,
|
test: /(?:\.ngfactory\.js|\.ngstyle\.js|\.ts)$/,
|
||||||
loader: '@ngtools/webpack',
|
loader: "@ngtools/webpack",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.(jpe?g|png|gif|svg)$/i,
|
test: /\.(jpe?g|png|gif|svg)$/i,
|
||||||
exclude: /.*(fontawesome-webfont)\.svg/,
|
exclude: /.*(bwi-font)\.svg/,
|
||||||
use: [{
|
generator: {
|
||||||
loader: 'file-loader',
|
filename: "images/[name][ext]",
|
||||||
options: {
|
},
|
||||||
name: '[name].[ext]',
|
type: "asset/resource",
|
||||||
outputPath: 'images/',
|
},
|
||||||
},
|
],
|
||||||
}],
|
},
|
||||||
},
|
plugins: [],
|
||||||
],
|
resolve: {
|
||||||
},
|
extensions: [".tsx", ".ts", ".js", ".json"],
|
||||||
plugins: [],
|
plugins: [new TsconfigPathsPlugin({ configFile: "./tsconfig.json" })],
|
||||||
resolve: {
|
symlinks: false,
|
||||||
extensions: ['.tsx', '.ts', '.js', '.json'],
|
modules: [path.resolve("node_modules")],
|
||||||
plugins: [new TsconfigPathsPlugin({ configFile: './tsconfig.json' })],
|
},
|
||||||
symlinks: false,
|
output: {
|
||||||
modules: [path.resolve('node_modules')],
|
filename: "[name].js",
|
||||||
},
|
path: path.resolve(__dirname, "build"),
|
||||||
output: {
|
},
|
||||||
filename: '[name].js',
|
|
||||||
path: path.resolve(__dirname, 'build'),
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderer = {
|
const renderer = {
|
||||||
mode: 'production',
|
mode: "production",
|
||||||
devtool: false,
|
devtool: false,
|
||||||
target: 'electron-renderer',
|
target: "electron-renderer",
|
||||||
node: {
|
node: {
|
||||||
__dirname: false,
|
__dirname: false,
|
||||||
},
|
},
|
||||||
entry: {
|
entry: {
|
||||||
'app/main': './src/app/main.ts',
|
"app/main": "./src/app/main.ts",
|
||||||
},
|
},
|
||||||
optimization: {
|
optimization: {
|
||||||
minimize: false,
|
minimize: false,
|
||||||
splitChunks: {
|
splitChunks: {
|
||||||
cacheGroups: {
|
cacheGroups: {
|
||||||
commons: {
|
commons: {
|
||||||
test: /[\\/]node_modules[\\/]/,
|
test: /[\\/]node_modules[\\/]/,
|
||||||
name: 'app/vendor',
|
name: "app/vendor",
|
||||||
chunks: (chunk) => {
|
chunks: (chunk) => {
|
||||||
return chunk.name === 'app/main';
|
return chunk.name === "app/main";
|
||||||
},
|
},
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
module: {
|
},
|
||||||
rules: [
|
module: {
|
||||||
{
|
rules: [
|
||||||
test: /\.(html)$/,
|
{
|
||||||
loader: 'html-loader',
|
test: /\.(html)$/,
|
||||||
},
|
loader: "html-loader",
|
||||||
{
|
},
|
||||||
test: /.(ttf|otf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/,
|
{
|
||||||
exclude: /loading.svg/,
|
test: /.(ttf|otf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/,
|
||||||
use: [{
|
exclude: /loading.svg/,
|
||||||
loader: 'file-loader',
|
generator: {
|
||||||
options: {
|
filename: "fonts/[name][ext]",
|
||||||
name: '[name].[ext]',
|
},
|
||||||
outputPath: 'fonts/',
|
type: "asset/resource",
|
||||||
},
|
},
|
||||||
}],
|
{
|
||||||
},
|
test: /\.scss$/,
|
||||||
{
|
use: [
|
||||||
test: /\.scss$/,
|
{
|
||||||
use: [
|
loader: MiniCssExtractPlugin.loader,
|
||||||
{
|
options: {
|
||||||
loader: MiniCssExtractPlugin.loader,
|
publicPath: "../",
|
||||||
options: {
|
|
||||||
publicPath: '../',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'css-loader',
|
|
||||||
'sass-loader',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
// Hide System.import warnings. ref: https://github.com/angular/angular/issues/21560
|
|
||||||
{
|
|
||||||
test: /[\/\\]@angular[\/\\].+\.js$/,
|
|
||||||
parser: { system: true },
|
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
"css-loader",
|
||||||
|
"sass-loader",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
plugins: [
|
// Hide System.import warnings. ref: https://github.com/angular/angular/issues/21560
|
||||||
new AngularCompilerPlugin({
|
{
|
||||||
tsConfigPath: 'tsconfig.json',
|
test: /[\/\\]@angular[\/\\].+\.js$/,
|
||||||
entryModule: 'src/app/app.module#AppModule',
|
parser: { system: true },
|
||||||
sourceMap: true,
|
},
|
||||||
}),
|
|
||||||
// ref: https://github.com/angular/angular/issues/20357
|
|
||||||
new webpack.ContextReplacementPlugin(/\@angular(\\|\/)core(\\|\/)fesm5/,
|
|
||||||
path.resolve(__dirname, './src')),
|
|
||||||
new HtmlWebpackPlugin({
|
|
||||||
template: './src/index.html',
|
|
||||||
filename: 'index.html',
|
|
||||||
chunks: ['app/vendor', 'app/main']
|
|
||||||
}),
|
|
||||||
new webpack.SourceMapDevToolPlugin({
|
|
||||||
include: ['app/main.js']
|
|
||||||
}),
|
|
||||||
new webpack.DefinePlugin({ 'global.GENTLY': false }),
|
|
||||||
new MiniCssExtractPlugin({
|
|
||||||
filename: '[name].[hash].css',
|
|
||||||
chunkFilename: '[id].[hash].css',
|
|
||||||
}),
|
|
||||||
],
|
],
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new AngularWebpackPlugin({
|
||||||
|
tsConfigPath: "tsconfig.json",
|
||||||
|
entryModule: "src/app/app.module#AppModule",
|
||||||
|
sourceMap: true,
|
||||||
|
}),
|
||||||
|
// ref: https://github.com/angular/angular/issues/20357
|
||||||
|
new webpack.ContextReplacementPlugin(
|
||||||
|
/\@angular(\\|\/)core(\\|\/)fesm5/,
|
||||||
|
path.resolve(__dirname, "./src")
|
||||||
|
),
|
||||||
|
new HtmlWebpackPlugin({
|
||||||
|
template: "./src/index.html",
|
||||||
|
filename: "index.html",
|
||||||
|
chunks: ["app/vendor", "app/main"],
|
||||||
|
}),
|
||||||
|
new webpack.SourceMapDevToolPlugin({
|
||||||
|
include: ["app/main.js"],
|
||||||
|
}),
|
||||||
|
new webpack.DefinePlugin({ "global.GENTLY": false }),
|
||||||
|
new MiniCssExtractPlugin({
|
||||||
|
filename: "[name].[contenthash].css",
|
||||||
|
chunkFilename: "[id].[contenthash].css",
|
||||||
|
}),
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = merge(common, renderer);
|
module.exports = merge(common, renderer);
|
||||||
|
|||||||
Reference in New Issue
Block a user