mirror of
https://github.com/bitwarden/directory-connector
synced 2025-12-05 23:53:21 +00:00
Compare commits
390 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
def3caa93b | ||
|
|
0b881a63d4 | ||
|
|
9dadfbd037 | ||
|
|
b0bf07d70e | ||
|
|
9136e3936b | ||
|
|
35aead6c0e | ||
|
|
615f3b82db | ||
|
|
baa441cb90 | ||
|
|
9ad683ca09 | ||
|
|
c2d1d12cd2 | ||
|
|
3b6bac7668 | ||
|
|
2be879548d | ||
|
|
033c346042 | ||
|
|
32a8e65fe8 | ||
|
|
b2d4d80181 | ||
|
|
56c1cb23a0 | ||
|
|
ba26f70d1a | ||
|
|
e5589e7664 | ||
|
|
4e82486784 | ||
|
|
bb1cdebaf4 | ||
|
|
01405f47c9 | ||
|
|
5e64dc9262 | ||
|
|
9c7cd943b3 | ||
|
|
7cf3166169 | ||
|
|
9bdb77a573 | ||
|
|
3b8ee5ec0d | ||
|
|
6e7e09064f | ||
|
|
dfcb450a8a | ||
|
|
b192c34c15 | ||
|
|
f813dbb690 | ||
|
|
16deafca76 | ||
|
|
647b087fa7 | ||
|
|
4bd1387b83 | ||
|
|
4e098462dc | ||
|
|
0b1c2ae72a | ||
|
|
5d3fa0a0d2 | ||
|
|
6097bca063 | ||
|
|
a6aafe7593 | ||
|
|
56d05af07a | ||
|
|
0d17345600 | ||
|
|
5df62b7422 | ||
|
|
868914feb1 | ||
|
|
1a9555d4af | ||
|
|
33c8f15e45 | ||
|
|
ed8dd01dbd | ||
|
|
2296e37e8f | ||
|
|
a0f33c7bdc | ||
|
|
f6b249836e | ||
|
|
0c92a97054 | ||
|
|
24ab152559 | ||
|
|
5f9f09d77c | ||
|
|
4d27d9e48d | ||
|
|
0b624b972a | ||
|
|
1889e12bac | ||
|
|
7648f73072 | ||
|
|
75d346ed85 | ||
|
|
dabfe7907d | ||
|
|
965976223f | ||
|
|
410f00c213 | ||
|
|
0d8b942ad4 | ||
|
|
5371015a58 | ||
|
|
2ead70e434 | ||
|
|
ffca14cb5f | ||
|
|
090d5e82df | ||
|
|
8893ddf0f7 | ||
|
|
997ec5a699 | ||
|
|
762818ee39 | ||
|
|
61c6ba8189 | ||
|
|
9cfa646bcb | ||
|
|
b4301c7d41 | ||
|
|
71b5f6a38a | ||
|
|
1c0052fe30 | ||
|
|
35862acb73 | ||
|
|
11cf64fcc7 | ||
|
|
2ab37b45cf | ||
|
|
7096fc830b | ||
|
|
39806b7d96 | ||
|
|
7a16b8cb0e | ||
|
|
2583068dbd | ||
|
|
e5d0b3a372 | ||
|
|
9a1caf1e7e | ||
|
|
af0e41e26c | ||
|
|
d3049164a9 | ||
|
|
bdfda6775d | ||
|
|
72e6e74a42 | ||
|
|
75832fbed9 | ||
|
|
811b3adb51 | ||
|
|
e710df3ba7 | ||
|
|
4ee0c3ccba | ||
|
|
036b934119 | ||
|
|
4bfd43bf4c | ||
|
|
55722d3c04 | ||
|
|
77043d8d66 | ||
|
|
002117a6e5 | ||
|
|
5aa8097cfd | ||
|
|
19e1049566 | ||
|
|
87bdc88e22 | ||
|
|
950e3ae91e | ||
|
|
a37532e1ad | ||
|
|
21ff5f311b | ||
|
|
7dd12cf0cb | ||
|
|
6f8df7a690 | ||
|
|
1ff0ef1f90 | ||
|
|
196fc10d80 | ||
|
|
7496de4cc6 | ||
|
|
02a6adf6a2 | ||
|
|
6c95575a8f | ||
|
|
39755e89a8 | ||
|
|
c5d3ca218e | ||
|
|
87a2a2a0e4 | ||
|
|
5904da2eb1 | ||
|
|
1ac0c81661 | ||
|
|
955711714d | ||
|
|
5848553a4b | ||
|
|
38758caac4 | ||
|
|
b41a1bdbf4 | ||
|
|
a400ab7f7d | ||
|
|
e5d0405882 | ||
|
|
f2137c02f7 | ||
|
|
4c61f465a3 | ||
|
|
6d22041eab | ||
|
|
752e26db6d | ||
|
|
094ec23f04 | ||
|
|
af8ff2901e | ||
|
|
628689c990 | ||
|
|
ab37221182 | ||
|
|
626892473f | ||
|
|
c621677852 | ||
|
|
6650b4848d | ||
|
|
c07c56f89b | ||
|
|
294590882f | ||
|
|
9151b9c2d6 | ||
|
|
5817468d09 | ||
|
|
31dd20999c | ||
|
|
4eb9c9bd4d | ||
|
|
150164534f | ||
|
|
2b2d8a9fab | ||
|
|
fb122cbbdb | ||
|
|
0b37857d29 | ||
|
|
15c1876687 | ||
|
|
473a6e391d | ||
|
|
13ad64e6f3 | ||
|
|
04e278249e | ||
|
|
e12c4ea1e2 | ||
|
|
acfa8632af | ||
|
|
f53c1f5605 | ||
|
|
f0f7f89ea8 | ||
|
|
059ff0647a | ||
|
|
d94e5b0620 | ||
|
|
3840bce6d7 | ||
|
|
71cd11eedf | ||
|
|
ecea04bc08 | ||
|
|
0575ee5507 | ||
|
|
8b2fa8405b | ||
|
|
7d36a50687 | ||
|
|
f7dd9d8d5b | ||
|
|
379b4f4612 | ||
|
|
4652668e1b | ||
|
|
4e02a8571e | ||
|
|
bc927a65ac | ||
|
|
90f1a1c115 | ||
|
|
c48acf6038 | ||
|
|
2640e8c890 | ||
|
|
094ec55e7f | ||
|
|
fe39bdac42 | ||
|
|
2c98d50c43 | ||
|
|
d374cff51c | ||
|
|
d9e7256804 | ||
|
|
0ef2d1523e | ||
|
|
05bad6f671 | ||
|
|
5a62cfcda1 | ||
|
|
634d38510d | ||
|
|
bf27872973 | ||
|
|
20bb5a4926 | ||
|
|
f63fb3ffa0 | ||
|
|
f11c32c606 | ||
|
|
0b4e22a952 | ||
|
|
a85dbff3a5 | ||
|
|
d2835dd577 | ||
|
|
62abc99b61 | ||
|
|
20de62cc79 | ||
|
|
731614279f | ||
|
|
2be751dc8e | ||
|
|
e0409941a3 | ||
|
|
e6a5a3c8c1 | ||
|
|
3f3590a223 | ||
|
|
c1f64d7b82 | ||
|
|
f90611c96f | ||
|
|
630e21f7c1 | ||
|
|
2e81642c0e | ||
|
|
39514b9550 | ||
|
|
2da82d5610 | ||
|
|
69f33a08b6 | ||
|
|
a05e9c7746 | ||
|
|
d8031e4f49 | ||
|
|
e6aa07ba5c | ||
|
|
173129014a | ||
|
|
8d4baa6d31 | ||
|
|
20463ce653 | ||
|
|
2617f96710 | ||
|
|
53b0614faf | ||
|
|
a01db9c448 | ||
|
|
84dc8e3696 | ||
|
|
e7b988c042 | ||
|
|
a06212af1a | ||
|
|
b217ac9456 | ||
|
|
ff8422d6c1 | ||
|
|
b51d279ba6 | ||
|
|
78e4601413 | ||
|
|
5900db10eb | ||
|
|
c9bd853032 | ||
|
|
976fcad098 | ||
|
|
5d8193c348 | ||
|
|
cc09b26818 | ||
|
|
b0247caa4c | ||
|
|
38316abeae | ||
|
|
0991dfa6bd | ||
|
|
88029663e9 | ||
|
|
3f5cb10db6 | ||
|
|
1c927722f0 | ||
|
|
c04354ba1c | ||
|
|
3335659c8d | ||
|
|
f0282b33f0 | ||
|
|
f69d461463 | ||
|
|
bcf16562fe | ||
|
|
d7412410f2 | ||
|
|
d5907477df | ||
|
|
9acfaff361 | ||
|
|
737274da59 | ||
|
|
7a0e0e6915 | ||
|
|
25847b9c83 | ||
|
|
07dd7bb93e | ||
|
|
bc9936bd92 | ||
|
|
e4428e3387 | ||
|
|
30ef0ce140 | ||
|
|
38c461ebe0 | ||
|
|
ead1776f90 | ||
|
|
0e944b2442 | ||
|
|
3bbd503308 | ||
|
|
a7b13b168e | ||
|
|
1cf8ae1bdf | ||
|
|
7da3c8e252 | ||
|
|
2edf3fb68d | ||
|
|
1e698ba81b | ||
|
|
e9034ea6fe | ||
|
|
08ebc1ce16 | ||
|
|
a80c95e1cf | ||
|
|
c3ab785d66 | ||
|
|
baef6210c1 | ||
|
|
74d4a6fa72 | ||
|
|
770e41a657 | ||
|
|
d04eb03b3c | ||
|
|
9a8224a8d6 | ||
|
|
bd072b727c | ||
|
|
93cedec32c | ||
|
|
3d8933e5f0 | ||
|
|
790f5a5f2f | ||
|
|
36bb24d580 | ||
|
|
3e08ba95cf | ||
|
|
e50f82175b | ||
|
|
5907c8b5ce | ||
|
|
9f9b5beff9 | ||
|
|
71dbd31477 | ||
|
|
9e4d17cc7c | ||
|
|
2ae14913cc | ||
|
|
a4e23eee83 | ||
|
|
0bc406b332 | ||
|
|
76a6b2407e | ||
|
|
f371067d56 | ||
|
|
d1507dffca | ||
|
|
c11cc0b979 | ||
|
|
7cc941cc84 | ||
|
|
a847339d72 | ||
|
|
46827cdaa0 | ||
|
|
183b54f9b8 | ||
|
|
e51a70f7b9 | ||
|
|
f83e97abfc | ||
|
|
92a566e73e | ||
|
|
59ade0d2ba | ||
|
|
7da368d555 | ||
|
|
e00fd648a6 | ||
|
|
f74c8420fb | ||
|
|
d2fbb8aba9 | ||
|
|
ba8aee2148 | ||
|
|
018dfd8865 | ||
|
|
be40ee3100 | ||
|
|
35f9b5ed17 | ||
|
|
8c4450246b | ||
|
|
537e7d10e6 | ||
|
|
124c7288b9 | ||
|
|
3224648bc7 | ||
|
|
9b8a11b65e | ||
|
|
6b9d72f4a2 | ||
|
|
8d50697345 | ||
|
|
f8e9d2b73a | ||
|
|
99ed78a2e6 | ||
|
|
346dab2c26 | ||
|
|
28582b2b0c | ||
|
|
2bda7e5f65 | ||
|
|
8849f6c513 | ||
|
|
6d50856c07 | ||
|
|
bdec0f9493 | ||
|
|
ab31beb1ae | ||
|
|
014d995284 | ||
|
|
69783d3606 | ||
|
|
55a0078332 | ||
|
|
346e0194d4 | ||
|
|
512f222819 | ||
|
|
006c6b3312 | ||
|
|
8c80a52ce2 | ||
|
|
5d8bf54f2f | ||
|
|
70c1eb0623 | ||
|
|
447b674469 | ||
|
|
7cb2147569 | ||
|
|
327de4d714 | ||
|
|
9b57955d72 | ||
|
|
e63198e130 | ||
|
|
03eac9006f | ||
|
|
d3402cca5e | ||
|
|
076b320e70 | ||
|
|
44180f95a5 | ||
|
|
e23ebf13b7 | ||
|
|
e0fd1a2e93 | ||
|
|
c39986b37c | ||
|
|
1e0029dc15 | ||
|
|
63bc5e4161 | ||
|
|
8c8b4da595 | ||
|
|
1a7536270a | ||
|
|
d92e3c8d7b | ||
|
|
c30aedf491 | ||
|
|
0e96a462ee | ||
|
|
3ef60cb3a0 | ||
|
|
34b7638e11 | ||
|
|
63ef932469 | ||
|
|
e09163fc72 | ||
|
|
6a737f6d7d | ||
|
|
ccc7b7b213 | ||
|
|
1e98bc6430 | ||
|
|
c63a945057 | ||
|
|
a13d9fa00f | ||
|
|
1518d5eafc | ||
|
|
c74800fd8c | ||
|
|
9e282efe28 | ||
|
|
26efc9158d | ||
|
|
e2d3d35d71 | ||
|
|
c115bf84f0 | ||
|
|
5b56d32492 | ||
|
|
2fe294ad8a | ||
|
|
1343897fa9 | ||
|
|
9b2b0e4ea6 | ||
|
|
489e775b08 | ||
|
|
0d76a45181 | ||
|
|
4787f9a462 | ||
|
|
b228e12c81 | ||
|
|
0e28100ec6 | ||
|
|
d2c1697144 | ||
|
|
b924ea8dd1 | ||
|
|
30936d3ad1 | ||
|
|
289589dce9 | ||
|
|
48734b8109 | ||
|
|
2e69a41943 | ||
|
|
f414a3ac0a | ||
|
|
81833b134c | ||
|
|
5a5b74a0d5 | ||
|
|
5a7a230193 | ||
|
|
bc5ab6252e | ||
|
|
0e4d5e0973 | ||
|
|
08e254e34c | ||
|
|
2c20796ea1 | ||
|
|
8b9d7a7e2e | ||
|
|
1a0709b02c | ||
|
|
0f386ee8d2 | ||
|
|
c0b41155e9 | ||
|
|
ddd1165728 | ||
|
|
651dbe59c8 | ||
|
|
aeb6f28f9a | ||
|
|
012eefad4e | ||
|
|
b05346d4a3 | ||
|
|
854e6e84fe | ||
|
|
20436bf07d | ||
|
|
4110e4b8cb | ||
|
|
e179edc2e0 | ||
|
|
133ef7eb16 | ||
|
|
5a98982fc2 | ||
|
|
f6e92bf597 | ||
|
|
af4a8d5ae8 | ||
|
|
c3704bbb67 | ||
|
|
8620f2f80d | ||
|
|
e0124474cf | ||
|
|
ac21b78225 |
BIN
.github/secrets/devid-app-cert.p12.gpg
vendored
Normal file
BIN
.github/secrets/devid-app-cert.p12.gpg
vendored
Normal file
Binary file not shown.
BIN
.github/secrets/devid-installer-cert.p12.gpg
vendored
Normal file
BIN
.github/secrets/devid-installer-cert.p12.gpg
vendored
Normal file
Binary file not shown.
BIN
.github/secrets/macdev-cert.p12.gpg
vendored
Normal file
BIN
.github/secrets/macdev-cert.p12.gpg
vendored
Normal file
Binary file not shown.
546
.github/workflows/build.yml
vendored
Normal file
546
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,546 @@
|
||||
---
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- 'l10n_master'
|
||||
|
||||
|
||||
jobs:
|
||||
cloc:
|
||||
name: CLOC
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
|
||||
|
||||
- name: Set up CLOC
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt -y install cloc
|
||||
|
||||
- name: Print lines of code
|
||||
run: cloc --include-lang TypeScript,JavaScript,HTML,Sass,CSS --vcs git
|
||||
|
||||
|
||||
setup:
|
||||
name: Setup
|
||||
runs-on: ubuntu-20.04
|
||||
outputs:
|
||||
package_version: ${{ steps.retrieve-version.outputs.package_version }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
|
||||
|
||||
- name: Get Package Version
|
||||
id: retrieve-version
|
||||
run: |
|
||||
PKG_VERSION=$(jq -r .version src/package.json)
|
||||
echo "::set-output name=package_version::$PKG_VERSION"
|
||||
|
||||
|
||||
cli:
|
||||
name: CLI
|
||||
runs-on: windows-2019
|
||||
needs: setup
|
||||
env:
|
||||
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
|
||||
_WIN_PKG_FETCH_VERSION: 14.17.6
|
||||
_WIN_PKG_VERSION: 3.2
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
|
||||
|
||||
- name: Setup Windows builder
|
||||
run: |
|
||||
choco install checksum --no-progress
|
||||
choco install reshack --no-progress
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
|
||||
with:
|
||||
node-version: '14'
|
||||
|
||||
- name: Update NPM
|
||||
run: |
|
||||
npm install -g npm@7
|
||||
npm install -g node-gyp
|
||||
node-gyp install $(node -v)
|
||||
|
||||
- name: Setting WIN_PKG
|
||||
run: |
|
||||
echo "WIN_PKG=$env:WIN_PKG" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
||||
echo "version: $env:pkgVersion"
|
||||
env:
|
||||
WIN_PKG: C:\Users\runneradmin\.pkg-cache\v3.0\fetched-v14.16.1-win-x64
|
||||
|
||||
- name: Get pkg-fetch
|
||||
shell: pwsh
|
||||
run: |
|
||||
cd $HOME
|
||||
$fetchedUrl = "https://github.com/vercel/pkg-fetch/releases/download/v$env:_WIN_PKG_VERSION/node-v$env:_WIN_PKG_FETCH_VERSION-win-x64"
|
||||
|
||||
New-Item -ItemType directory -Path ./.pkg-cache
|
||||
New-Item -ItemType directory -Path ./.pkg-cache/v$env:_WIN_PKG_VERSION
|
||||
Invoke-RestMethod -Uri $fetchedUrl `
|
||||
-OutFile "./.pkg-cache/v$env:_WIN_PKG_VERSION/fetched-v$env:_WIN_PKG_FETCH_VERSION-win-x64"
|
||||
|
||||
- name: Keytar
|
||||
shell: pwsh
|
||||
run: |
|
||||
$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}-node-v${nodeModVersion}-{0}-x64.tar"
|
||||
$keytarTarGz = "${keytarTar}.gz"
|
||||
$keytarUrl = "https://github.com/atom/node-keytar/releases/download/v${keytarVersion}/${keytarTarGz}"
|
||||
|
||||
New-Item -ItemType directory -Path ./keytar/macos | Out-Null
|
||||
New-Item -ItemType directory -Path ./keytar/linux | Out-Null
|
||||
New-Item -ItemType directory -Path ./keytar/windows | Out-Null
|
||||
|
||||
Invoke-RestMethod -Uri $($keytarUrl -f "darwin") -OutFile "./keytar/macos/$($keytarTarGz -f "darwin")"
|
||||
Invoke-RestMethod -Uri $($keytarUrl -f "linux") -OutFile "./keytar/linux/$($keytarTarGz -f "linux")"
|
||||
Invoke-RestMethod -Uri $($keytarUrl -f "win32") -OutFile "./keytar/windows/$($keytarTarGz -f "win32")"
|
||||
|
||||
7z e "./keytar/macos/$($keytarTarGz -f "darwin")" -o"./keytar/macos"
|
||||
7z e "./keytar/linux/$($keytarTarGz -f "linux")" -o"./keytar/linux"
|
||||
7z e "./keytar/windows/$($keytarTarGz -f "win32")" -o"./keytar/windows"
|
||||
|
||||
7z e "./keytar/macos/$($keytarTar -f "darwin")" -o"./keytar/macos"
|
||||
7z e "./keytar/linux/$($keytarTar -f "linux")" -o"./keytar/linux"
|
||||
7z e "./keytar/windows/$($keytarTar -f "win32")" -o"./keytar/windows"
|
||||
|
||||
- name: Setup Version Info
|
||||
shell: pwsh
|
||||
run: |
|
||||
$major, $minor, $patch = $env:_PACKAGE_VERSION.split('.')
|
||||
|
||||
$versionInfo = @"
|
||||
|
||||
1 VERSIONINFO
|
||||
FILEVERSION $major,$minor,$patch,0
|
||||
PRODUCTVERSION $major,$minor,$patch,0
|
||||
FILEOS 0x40004
|
||||
FILETYPE 0x1
|
||||
{
|
||||
BLOCK "StringFileInfo"
|
||||
{
|
||||
BLOCK "040904b0"
|
||||
{
|
||||
VALUE "CompanyName", "Bitwarden Inc."
|
||||
VALUE "ProductName", "Bitwarden"
|
||||
VALUE "FileDescription", "Bitwarden Directory Connector CLI"
|
||||
VALUE "FileVersion", "$env:_PACKAGE_VERSION"
|
||||
VALUE "ProductVersion", "$env:_PACKAGE_VERSION"
|
||||
VALUE "OriginalFilename", "bwdc.exe"
|
||||
VALUE "InternalName", "bwdc"
|
||||
VALUE "LegalCopyright", "Copyright Bitwarden Inc."
|
||||
}
|
||||
}
|
||||
|
||||
BLOCK "VarFileInfo"
|
||||
{
|
||||
VALUE "Translation", 0x0409 0x04B0
|
||||
}
|
||||
}
|
||||
"@
|
||||
|
||||
$versionInfo | Out-File ./version-info.rc
|
||||
|
||||
- name: Resource Hacker
|
||||
shell: cmd
|
||||
run: |
|
||||
set PATH=%PATH%;C:\Program Files (x86)\Resource Hacker
|
||||
set WIN_PKG=C:\Users\runneradmin\.pkg-cache\v%_WIN_PKG_VERSION%\fetched-v%_WIN_PKG_FETCH_VERSION%-win-x64
|
||||
set WIN_PKG_BUILT=C:\Users\runneradmin\.pkg-cache\v%_WIN_PKG_VERSION%\built-v%_WIN_PKG_FETCH_VERSION%-win-x64
|
||||
|
||||
ResourceHacker -open %WIN_PKG% -save %WIN_PKG% -action delete -mask ICONGROUP,1,
|
||||
ResourceHacker -open version-info.rc -save version-info.res -action compile
|
||||
ResourceHacker -open %WIN_PKG% -save %WIN_PKG% -action addoverwrite -resource version-info.res
|
||||
|
||||
- name: Install
|
||||
run: npm install
|
||||
|
||||
- name: Package CLI
|
||||
run: npm run dist:cli
|
||||
|
||||
- name: Zip
|
||||
shell: cmd
|
||||
run: |
|
||||
7z a ./dist-cli/bwdc-windows-%_PACKAGE_VERSION%.zip ./dist-cli/windows/bwdc.exe ./keytar/windows/keytar.node
|
||||
7z a ./dist-cli/bwdc-macos-%_PACKAGE_VERSION%.zip ./dist-cli/macos/bwdc ./keytar/macos/keytar.node
|
||||
7z a ./dist-cli/bwdc-linux-%_PACKAGE_VERSION%.zip ./dist-cli/linux/bwdc ./keytar/linux/keytar.node
|
||||
|
||||
- name: Version Test
|
||||
run: |
|
||||
Expand-Archive -Path "./dist-cli/bwdc-windows-${env:_PACKAGE_VERSION}.zip" -DestinationPath "./test/windows"
|
||||
$testVersion = Invoke-Expression '& ./test/windows/bwdc.exe -v'
|
||||
echo "version: $env:_PACKAGE_VERSION"
|
||||
echo "testVersion: $testVersion"
|
||||
if($testVersion -ne $env:_PACKAGE_VERSION) {
|
||||
Throw "Version test failed."
|
||||
}
|
||||
|
||||
- name: Create checksums
|
||||
run: |
|
||||
checksum -f="./dist-cli/bwdc-windows-${env:_PACKAGE_VERSION}.zip" `
|
||||
-t sha256 | Out-File ./dist-cli/bwdc-windows-sha256-${env:_PACKAGE_VERSION}.txt
|
||||
checksum -f="./dist-cli/bwdc-macos-${env:_PACKAGE_VERSION}.zip" `
|
||||
-t sha256 | Out-File ./dist-cli/bwdc-macos-sha256-${env:_PACKAGE_VERSION}.txt
|
||||
checksum -f="./dist-cli/bwdc-linux-${env:_PACKAGE_VERSION}.zip" `
|
||||
-t sha256 | Out-File ./dist-cli/bwdc-linux-sha256-${env:_PACKAGE_VERSION}.txt
|
||||
|
||||
- name: Upload Windows Zip to GitHub
|
||||
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
|
||||
with:
|
||||
name: bwdc-windows-${{ env._PACKAGE_VERSION }}.zip
|
||||
path: ./dist-cli/bwdc-windows-${{ env._PACKAGE_VERSION }}.zip
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload Mac Zip to GitHub
|
||||
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
|
||||
with:
|
||||
name: bwdc-macos-${{ env._PACKAGE_VERSION }}.zip
|
||||
path: ./dist-cli/bwdc-macos-${{ env._PACKAGE_VERSION }}.zip
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload Linux Zip to GitHub
|
||||
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
|
||||
with:
|
||||
name: bwdc-linux-${{ env._PACKAGE_VERSION }}.zip
|
||||
path: ./dist-cli/bwdc-linux-${{ env._PACKAGE_VERSION }}.zip
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload Windows checksum to GitHub
|
||||
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
|
||||
with:
|
||||
name: bwdc-windows-sha256-${{ env._PACKAGE_VERSION }}.txt
|
||||
path: ./dist-cli/bwdc-windows-sha256-${{ env._PACKAGE_VERSION }}.txt
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload Mac checksum to GitHub
|
||||
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
|
||||
with:
|
||||
name: bwdc-macos-sha256-${{ env._PACKAGE_VERSION }}.txt
|
||||
path: ./dist-cli/bwdc-macos-sha256-${{ env._PACKAGE_VERSION }}.txt
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload Linux checksum to GitHub
|
||||
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
|
||||
with:
|
||||
name: bwdc-linux-sha256-${{ env._PACKAGE_VERSION }}.txt
|
||||
path: ./dist-cli/bwdc-linux-sha256-${{ env._PACKAGE_VERSION }}.txt
|
||||
if-no-files-found: error
|
||||
|
||||
|
||||
windows-gui:
|
||||
name: Windows GUI
|
||||
runs-on: windows-2019
|
||||
needs: setup
|
||||
env:
|
||||
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
|
||||
steps:
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@a71d1eb2c86af85faa8c772c03fb365e377e45ea
|
||||
with:
|
||||
dotnet-version: "3.1.x"
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
|
||||
with:
|
||||
node-version: '14'
|
||||
|
||||
- name: Update NPM
|
||||
run: |
|
||||
npm install -g npm@7
|
||||
npm install -g node-gyp
|
||||
node-gyp install $(node -v)
|
||||
|
||||
- name: Set Node options
|
||||
run: echo "NODE_OPTIONS=--max_old_space_size=4096" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
||||
shell: pwsh
|
||||
|
||||
- name: Print environment
|
||||
run: |
|
||||
node --version
|
||||
npm --version
|
||||
dotnet --version
|
||||
|
||||
- name: Install AST
|
||||
uses: bitwarden/gh-actions/install-ast@f135c42c8596cb535c5bcb7523c0b2eef89709ac
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
|
||||
|
||||
- name: Install Node dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Run linter
|
||||
run: npm run lint
|
||||
|
||||
- name: Build & Sign
|
||||
run: npm run dist:win
|
||||
env:
|
||||
ELECTRON_BUILDER_SIGN: 1
|
||||
SIGNING_VAULT_URL: ${{ secrets.SIGNING_VAULT_URL }}
|
||||
SIGNING_CLIENT_ID: ${{ secrets.SIGNING_CLIENT_ID }}
|
||||
SIGNING_TENANT_ID: ${{ secrets.SIGNING_TENANT_ID }}
|
||||
SIGNING_CLIENT_SECRET: ${{ secrets.SIGNING_CLIENT_SECRET }}
|
||||
SIGNING_CERT_NAME: ${{ secrets.SIGNING_CERT_NAME }}
|
||||
|
||||
- name: List Dist
|
||||
run: dir ./dist
|
||||
|
||||
- name: Upload Portable Executable to GitHub
|
||||
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
|
||||
with:
|
||||
name: Bitwarden-Connector-Portable-${{ env._PACKAGE_VERSION }}.exe
|
||||
path: ./dist/Bitwarden-Connector-Portable-${{ env._PACKAGE_VERSION }}.exe
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload Installer Executable to GitHub
|
||||
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
|
||||
with:
|
||||
name: Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe
|
||||
path: ./dist/Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload latest auto-update artifact
|
||||
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
|
||||
with:
|
||||
name: latest.yml
|
||||
path: ./dist/latest.yml
|
||||
if-no-files-found: error
|
||||
|
||||
|
||||
linux:
|
||||
name: Linux
|
||||
runs-on: ubuntu-20.04
|
||||
needs: setup
|
||||
env:
|
||||
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
|
||||
steps:
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
|
||||
with:
|
||||
node-version: '14'
|
||||
|
||||
- name: Update NPM
|
||||
run: |
|
||||
npm install -g npm@7
|
||||
npm install -g node-gyp
|
||||
node-gyp install $(node -v)
|
||||
|
||||
- name: Set Node options
|
||||
run: echo "NODE_OPTIONS=--max_old_space_size=4096" >> $GITHUB_ENV
|
||||
|
||||
- name: Set up environment
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get -y install pkg-config libxss-dev libsecret-1-dev
|
||||
sudo apt-get -y install rpm
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
|
||||
|
||||
- name: NPM Install
|
||||
run: npm install
|
||||
|
||||
- name: NPM Rebuild
|
||||
run: npm run rebuild
|
||||
|
||||
- name: NPM Package
|
||||
run: npm run dist:lin
|
||||
|
||||
- name: Upload AppImage
|
||||
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
|
||||
with:
|
||||
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-x86_64.AppImage
|
||||
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-x86_64.AppImage
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload latest auto-update artifact
|
||||
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
|
||||
with:
|
||||
name: latest-linux.yml
|
||||
path: ./dist/latest-linux.yml
|
||||
if-no-files-found: error
|
||||
|
||||
|
||||
macos:
|
||||
name: MacOS
|
||||
runs-on: macos-11
|
||||
needs: setup
|
||||
env:
|
||||
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
|
||||
steps:
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
|
||||
with:
|
||||
node-version: '14'
|
||||
|
||||
- name: Update NPM
|
||||
run: |
|
||||
npm install -g npm@7
|
||||
npm install -g node-gyp
|
||||
node-gyp install $(node -v)
|
||||
|
||||
- name: Set Node options
|
||||
run: echo "NODE_OPTIONS=--max_old_space_size=4096" >> $GITHUB_ENV
|
||||
|
||||
- name: Print environment
|
||||
run: |
|
||||
node --version
|
||||
npm --version
|
||||
echo "GitHub ref: $GITHUB_REF"
|
||||
echo "GitHub event: $GITHUB_EVENT"
|
||||
shell: bash
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
|
||||
|
||||
- name: Decrypt secrets
|
||||
env:
|
||||
DECRYPT_FILE_PASSWORD: ${{ secrets.DECRYPT_FILE_PASSWORD }}
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p $HOME/secrets
|
||||
|
||||
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
||||
--output "$HOME/secrets/devid-app-cert.p12" \
|
||||
"$GITHUB_WORKSPACE/.github/secrets/devid-app-cert.p12.gpg"
|
||||
|
||||
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
||||
--output "$HOME/secrets/devid-installer-cert.p12" \
|
||||
"$GITHUB_WORKSPACE/.github/secrets/devid-installer-cert.p12.gpg"
|
||||
|
||||
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
||||
--output "$HOME/secrets/macdev-cert.p12" \
|
||||
"$GITHUB_WORKSPACE/.github/secrets/macdev-cert.p12.gpg"
|
||||
|
||||
- name: Set up keychain
|
||||
env:
|
||||
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
|
||||
DEVID_CERT_PASSWORD: ${{ secrets.DEVID_CERT_PASSWORD }}
|
||||
MACDEV_CERT_PASSWORD: ${{ secrets.MACDEV_CERT_PASSWORD }}
|
||||
shell: bash
|
||||
run: |
|
||||
security create-keychain -p $KEYCHAIN_PASSWORD build.keychain
|
||||
security default-keychain -s build.keychain
|
||||
security unlock-keychain -p $KEYCHAIN_PASSWORD build.keychain
|
||||
security set-keychain-settings -lut 1200 build.keychain
|
||||
security import "$HOME/secrets/devid-app-cert.p12" -k build.keychain -P $DEVID_CERT_PASSWORD \
|
||||
-T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild
|
||||
security import "$HOME/secrets/devid-installer-cert.p12" -k build.keychain -P $DEVID_CERT_PASSWORD \
|
||||
-T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild
|
||||
security import "$HOME/secrets/macdev-cert.p12" -k build.keychain -P $MACDEV_CERT_PASSWORD \
|
||||
-T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild
|
||||
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $KEYCHAIN_PASSWORD build.keychain
|
||||
|
||||
- name: Load package version
|
||||
run: |
|
||||
$rootPath = $env:GITHUB_WORKSPACE;
|
||||
$packageVersion = (Get-Content -Raw -Path $rootPath\src\package.json | ConvertFrom-Json).version;
|
||||
|
||||
Write-Output "Setting package version to $packageVersion";
|
||||
Write-Output "PACKAGE_VERSION=$packageVersion" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append;
|
||||
shell: pwsh
|
||||
|
||||
- name: Install Node dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Run linter
|
||||
run: npm run lint
|
||||
|
||||
- name: Build application
|
||||
run: npm run dist:mac
|
||||
env:
|
||||
APPLE_ID_USERNAME: ${{ secrets.APPLE_ID_USERNAME }}
|
||||
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
|
||||
|
||||
- name: Rename Zip Artifact
|
||||
run: |
|
||||
cd dist
|
||||
mv "Bitwarden Directory Connector-${{ env._PACKAGE_VERSION }}-mac.zip" \
|
||||
"Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-mac.zip"
|
||||
|
||||
- name: Upload .zip artifact
|
||||
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
|
||||
with:
|
||||
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-mac.zip
|
||||
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-mac.zip
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload .dmg artifact
|
||||
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
|
||||
with:
|
||||
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg
|
||||
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload latest auto-update artifact
|
||||
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
|
||||
with:
|
||||
name: latest-mac.yml
|
||||
path: ./dist/latest-mac.yml
|
||||
if-no-files-found: error
|
||||
|
||||
|
||||
check-failures:
|
||||
name: Check for failures
|
||||
if: always()
|
||||
runs-on: ubuntu-20.04
|
||||
needs:
|
||||
- cloc
|
||||
- setup
|
||||
- cli
|
||||
- windows-gui
|
||||
- linux
|
||||
- macos
|
||||
steps:
|
||||
- name: Check if any job failed
|
||||
if: ${{ (github.ref == 'refs/heads/master') || (github.ref == 'refs/heads/rc') }}
|
||||
env:
|
||||
CLOC_STATUS: ${{ needs.cloc.result }}
|
||||
SETUP_STATUS: ${{ needs.setup.result }}
|
||||
CLI_STATUS: ${{ needs.cli.result }}
|
||||
WINDOWS_GUI_STATUS: ${{ needs.windows-gui.result }}
|
||||
LINUX_STATUS: ${{ needs.linux.result }}
|
||||
MACOS_STATUS: ${{ needs.macos.result }}
|
||||
run: |
|
||||
if [ "$CLOC_STATUS" = "failure" ]; then
|
||||
exit 1
|
||||
elif [ "$SETUP_STATUS" = "failure" ]; then
|
||||
exit 1
|
||||
elif [ "$CLI_STATUS" = "failure" ]; then
|
||||
exit 1
|
||||
elif [ "$WINDOWS_GUI_STATUS" = "failure" ]; then
|
||||
exit 1
|
||||
elif [ "$LINUX_STATUS" = "failure" ]; then
|
||||
exit 1
|
||||
elif [ "$MACOS_STATUS" = "failure" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Login to Azure - Prod Subscription
|
||||
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
|
||||
if: failure()
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
||||
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
uses: Azure/get-keyvault-secrets@80ccd3fafe5662407cc2e55f202ee34bfff8c403
|
||||
if: failure()
|
||||
with:
|
||||
keyvault: "bitwarden-prod-kv"
|
||||
secrets: "devops-alerts-slack-webhook-url"
|
||||
|
||||
- name: Notify Slack on failure
|
||||
uses: act10ns/slack@e4e71685b9b239384b0f676a63c32367f59c2522 # v1.2.2
|
||||
if: failure()
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }}
|
||||
with:
|
||||
status: ${{ job.status }}
|
||||
78
.github/workflows/release.yml
vendored
Normal file
78
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
---
|
||||
name: Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
name: Setup
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Branch check
|
||||
run: |
|
||||
if [[ "$GITHUB_REF" != "refs/heads/release" ]]; then
|
||||
echo "==================================="
|
||||
echo "[!] Can only release from the 'release' branch"
|
||||
echo "==================================="
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
||||
with:
|
||||
ref: release
|
||||
|
||||
- name: Retrieve Directory Connector release version
|
||||
id: retrieve-version
|
||||
run: |
|
||||
PKG_VERSION=$(jq -r .version src/package.json)
|
||||
echo "::set-output name=package_version::$PKG_VERSION"
|
||||
|
||||
- name: Check to make sure Mobile release version has been bumped
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
latest_ver=$(hub release -L 1 -f '%T')
|
||||
latest_ver=${latest_ver:1}
|
||||
echo "Latest version: $latest_ver"
|
||||
ver=${{ steps.retrieve-version.outputs.package_version }}
|
||||
echo "Version: $ver"
|
||||
if [ "$latest_ver" = "$ver" ]; then
|
||||
echo "Version has not been bumped!"
|
||||
exit 1
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- name: Download all artifacts
|
||||
uses: bitwarden/gh-actions/download-artifacts@23433be15ed6fd046ce12b6889c5184a8d9c8783
|
||||
with:
|
||||
workflow: build.yml
|
||||
workflow_conclusion: success
|
||||
branch: release
|
||||
|
||||
- name: Create release
|
||||
uses: ncipollo/release-action@95215a3cb6e6a1908b3c44e00b4fdb15548b1e09 # v2.8.5
|
||||
env:
|
||||
PKG_VERSION: ${{ steps.retrieve-version.outputs.package_version }}
|
||||
with:
|
||||
artifacts: "./bwdc-windows-${{ env.PKG_VERSION }}.zip,
|
||||
./bwdc-macos-${{ env.PKG_VERSION }}.zip,
|
||||
./bwdc-linux-${{ env.PKG_VERSION }}.zip,
|
||||
./bwdc-windows-sha256-${{ env.PKG_VERSION }}.txt,
|
||||
./bwdc-macos-sha256-${{ env.PKG_VERSION }}.txt,
|
||||
./bwdc-linux-sha256-${{ env.PKG_VERSION }}.txt,
|
||||
./Bitwarden-Connector-Portable-${{ env.PKG_VERSION }}.exe,
|
||||
./Bitwarden-Connector-Installer-${{ env.PKG_VERSION }}.exe,
|
||||
./Bitwarden-Connector-${{ env.PKG_VERSION }}-x86_64.AppImage,
|
||||
./Bitwarden-Connector-${{ env.PKG_VERSION }}-mac.zip,
|
||||
./Bitwarden-Connector-${{ env.PKG_VERSION }}.dmg,
|
||||
./latest-linux.yml,
|
||||
./latest-mac.yml,
|
||||
./latest.yml"
|
||||
commit: ${{ github.sha }}
|
||||
tag: v${{ env.PKG_VERSION }}
|
||||
name: Version ${{ env.PKG_VERSION }}
|
||||
body: "<insert release notes here>"
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
draft: true
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -4,10 +4,14 @@ node_modules
|
||||
npm-debug.log
|
||||
vwd.webinfo
|
||||
dist/
|
||||
dist-cli/
|
||||
css/
|
||||
*.crx
|
||||
*.pem
|
||||
build-cli/
|
||||
build/
|
||||
yarn-error.log
|
||||
.DS_Store
|
||||
*.nupkg
|
||||
*.provisionprofile
|
||||
*.env
|
||||
|
||||
13
.vscode/launch.json
vendored
13
.vscode/launch.json
vendored
@@ -23,7 +23,18 @@
|
||||
"port": 9223,
|
||||
"webRoot": "${workspaceFolder}/build",
|
||||
"sourceMaps": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Debug CLI",
|
||||
"protocol": "inspector",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"program": "${workspaceFolder}/build-cli/bwdc.js",
|
||||
"args": [
|
||||
"sync"
|
||||
]
|
||||
}
|
||||
],
|
||||
"compounds": [
|
||||
{
|
||||
|
||||
53
README.md
53
README.md
@@ -1,4 +1,4 @@
|
||||
[](https://ci.appveyor.com/project/bitwarden/directory-connector)
|
||||

|
||||
[](https://gitter.im/bitwarden/Lobby)
|
||||
|
||||
# Bitwarden Directory Connector
|
||||
@@ -14,25 +14,66 @@ Supported directories:
|
||||
|
||||
The application is written using Electron with Angular and installs on Windows, macOS, and Linux distributions.
|
||||
|
||||
<a href="https://help.bitwarden.com/article/directory-sync/#download-and-install"><img src="https://imgur.com/SLv9paA.png" width="500" height="113"></a>
|
||||
[](https://help.bitwarden.com/article/directory-sync/#download-and-install)
|
||||
|
||||

|
||||

|
||||
|
||||
# Build/Run
|
||||
## Command-line Interface
|
||||
|
||||
A command-line interface tool is also available for the Bitwarden Directory Connector. The Directory Connector CLI (`bwdc`) is written with TypeScript and Node.js and can also be run on Windows, macOS, and Linux distributions.
|
||||
|
||||
## CLI Documentation
|
||||
|
||||
The Bitwarden Directory Connector CLI is self-documented with `--help` content and examples for every command. You should start exploring the CLI by using the global `--help` option:
|
||||
|
||||
```bash
|
||||
bwdc --help
|
||||
```
|
||||
|
||||
This option will list all available commands that you can use with the Directory Connector CLI.
|
||||
|
||||
Additionally, you can run the `--help` option on a specific command to learn more about it:
|
||||
|
||||
```
|
||||
bwdc test --help
|
||||
bwdc config --help
|
||||
```
|
||||
|
||||
**Detailed Documentation**
|
||||
|
||||
We provide detailed documentation and examples for using the Directory Connector CLI in our help center at https://help.bitwarden.com/article/directory-sync/#command-line-interface.
|
||||
|
||||
## Build/Run
|
||||
|
||||
**Requirements**
|
||||
|
||||
- [Node.js](https://nodejs.org/)
|
||||
- [Node.js](https://nodejs.org) v14
|
||||
- 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**
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run reset # Only necessary if you have previously run the CLI app
|
||||
npm run rebuild
|
||||
npm run electron
|
||||
```
|
||||
|
||||
# Contribute
|
||||
**Run the CLI**
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run reset # Only necessary if you have previously run the desktop app
|
||||
npm run build:cli:watch
|
||||
```
|
||||
|
||||
You can then run commands from the `./build-cli` folder:
|
||||
|
||||
```bash
|
||||
node ./build-cli/bwdc.js --help
|
||||
```
|
||||
|
||||
## Contribute
|
||||
|
||||
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.
|
||||
|
||||
|
||||
2
jslib
2
jslib
Submodule jslib updated: e0d5a4d8b7...cb00604617
31426
package-lock.json
generated
31426
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
175
package.json
175
package.json
@@ -9,7 +9,7 @@
|
||||
"vault",
|
||||
"password manager"
|
||||
],
|
||||
"author": "8bit Solutions LLC <hello@bitwarden.com> (https://bitwarden.com)",
|
||||
"author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)",
|
||||
"homepage": "https://bitwarden.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -21,37 +21,61 @@
|
||||
"sub:update": "git submodule update --remote",
|
||||
"sub:pull": "git submodule foreach git pull origin master",
|
||||
"sub:commit": "npm run sub:pull && git commit -am \"update submodule\"",
|
||||
"postinstall": "./node_modules/.bin/electron-rebuild && npm run sub:init",
|
||||
"lint": "tslint src/**/*.ts || true",
|
||||
"lint:fix": "tslint src/**/*.ts --fix",
|
||||
"preinstall": "npm run sub:init",
|
||||
"symlink:win": "rm -rf ./jslib && cmd /c mklink /J .\\jslib ..\\jslib",
|
||||
"symlink:mac": "npm run symlink:lin",
|
||||
"symlink:lin": "rm -rf ./jslib && ln -s ../jslib ./jslib",
|
||||
"rebuild": "electron-rebuild",
|
||||
"reset": "rimraf ./node_modules/keytar/* && npm install",
|
||||
"lint": "tslint 'src/**/*.ts' || true",
|
||||
"lint:fix": "tslint 'src/**/*.ts' --fix",
|
||||
"build": "concurrently -n Main,Rend -c yellow,cyan \"npm run build:main\" \"npm run build:renderer\"",
|
||||
"build:main": "webpack --config webpack.main.js",
|
||||
"build:renderer": "webpack --config webpack.renderer.js",
|
||||
"build:renderer:watch": "webpack --config webpack.renderer.js --watch",
|
||||
"build:dist": "npm run reset && npm run rebuild && npm run build",
|
||||
"build:cli": "webpack --config webpack.cli.js",
|
||||
"build:cli:watch": "webpack --config webpack.cli.js --watch",
|
||||
"build:cli:prod": "cross-env NODE_ENV=production webpack --config webpack.cli.js",
|
||||
"build:cli:prod:watch": "cross-env NODE_ENV=production webpack --config webpack.cli.js --watch",
|
||||
"electron": "npm run build:main && concurrently -k -n Main,Rend -c yellow,cyan \"electron --inspect=5858 ./build --watch\" \"npm run build:renderer:watch\"",
|
||||
"clean:dist": "rimraf ./dist/*",
|
||||
"pack:lin": "npm run clean:dist && build --linux --x64 -p never",
|
||||
"pack:mac": "npm run clean:dist && build --mac -p never",
|
||||
"pack:win": "npm run clean:dist && build --win --x64 --ia32 -p never -c.win.certificateSubjectName=\"8bit Solutions LLC\"",
|
||||
"pack:win:ci": "npm run clean:dist && build --win --x64 --ia32 -p never",
|
||||
"dist:lin": "npm run build && npm run pack:lin",
|
||||
"dist:mac": "npm run build && npm run pack:mac",
|
||||
"dist:win": "npm run build && npm run pack:win",
|
||||
"clean:dist:cli": "rimraf ./dist-cli/*",
|
||||
"pack:lin": "npm run clean:dist && electron-builder --linux --x64 -p never",
|
||||
"pack:mac": "npm run clean:dist && electron-builder --mac -p never",
|
||||
"pack:win": "npm run clean:dist && electron-builder --win --x64 --ia32 -p never -c.win.certificateSubjectName=\"8bit Solutions LLC\"",
|
||||
"pack:win:ci": "npm run clean:dist && electron-builder --win --x64 --ia32 -p never",
|
||||
"pack:cli": "npm run pack:cli:win | npm run pack:cli:mac | npm run pack:cli:lin",
|
||||
"pack:cli:win": "pkg ./src-cli --targets win-x64 --output ./dist-cli/windows/bwdc.exe",
|
||||
"pack:cli:mac": "pkg ./src-cli --targets macos-x64 --output ./dist-cli/macos/bwdc",
|
||||
"pack:cli:lin": "pkg ./src-cli --targets linux-x64 --output ./dist-cli/linux/bwdc",
|
||||
"dist:lin": "npm run build:dist && npm run pack:lin",
|
||||
"dist:mac": "npm run build:dist && npm run pack:mac",
|
||||
"dist:win": "npm run build:dist && npm run pack:win",
|
||||
"dist:win:ci": "npm run build && npm run pack:win:ci",
|
||||
"publish:lin": "npm run build && npm run clean:dist && build --linux --x64 -p always",
|
||||
"publish:mac": "npm run build && npm run clean:dist && build --mac -p always",
|
||||
"publish:win": "npm run build && npm run clean:dist && build --win --x64 --ia32 -p always -c.win.certificateSubjectName=\"8bit Solutions LLC\""
|
||||
"dist:cli": "npm run build:cli:prod && npm run clean:dist:cli && npm run pack:cli",
|
||||
"dist:cli:win": "npm run build:cli:prod && npm run clean:dist:cli && npm run pack:cli:win",
|
||||
"dist:cli:mac": "npm run build:cli:prod && npm run clean:dist:cli && npm run pack:cli:mac",
|
||||
"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: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\""
|
||||
},
|
||||
"build": {
|
||||
"appId": "com.bitwarden.directory-connector",
|
||||
"copyright": "Copyright © 2015-2018 8bit Solutions LLC",
|
||||
"copyright": "Copyright © 2015-2020 Bitwarden Inc.",
|
||||
"directories": {
|
||||
"buildResources": "resources",
|
||||
"output": "dist",
|
||||
"app": "build"
|
||||
},
|
||||
"afterSign": "scripts/notarize.js",
|
||||
"mac": {
|
||||
"category": "public.app-category.productivity",
|
||||
"gatekeeperAssess": false,
|
||||
"hardenedRuntime": true,
|
||||
"entitlements": "resources/entitlements.mac.plist",
|
||||
"entitlementsInherit": "resources/entitlements.mac.plist",
|
||||
"target": [
|
||||
"dmg",
|
||||
"zip"
|
||||
@@ -61,7 +85,8 @@
|
||||
"target": [
|
||||
"portable",
|
||||
"nsis"
|
||||
]
|
||||
],
|
||||
"sign": "scripts/sign.js"
|
||||
},
|
||||
"linux": {
|
||||
"category": "Utility",
|
||||
@@ -107,68 +132,66 @@
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/compiler-cli": "5.2.0",
|
||||
"@microsoft/microsoft-graph-types": "^1.2.0",
|
||||
"@ngtools/webpack": "1.10.2",
|
||||
"@types/keytar": "^4.0.1",
|
||||
"@types/ldapjs": "^1.0.3",
|
||||
"@types/lowdb": "^1.0.1",
|
||||
"@types/lunr": "2.1.5",
|
||||
"@types/node": "8.0.19",
|
||||
"@types/node-forge": "0.7.1",
|
||||
"@types/webcrypto": "0.0.28",
|
||||
"clean-webpack-plugin": "^0.1.17",
|
||||
"concurrently": "3.5.1",
|
||||
"copy-webpack-plugin": "^4.2.0",
|
||||
"css-loader": "^0.28.7",
|
||||
"electron": "2.0.2",
|
||||
"electron-builder": "^20.8.1",
|
||||
"electron-rebuild": "1.7.3",
|
||||
"electron-reload": "1.2.2",
|
||||
"extract-text-webpack-plugin": "^3.0.1",
|
||||
"file-loader": "^1.1.5",
|
||||
"@angular/compiler-cli": "^11.2.11",
|
||||
"@microsoft/microsoft-graph-types": "^1.4.0",
|
||||
"@ngtools/webpack": "^11.2.10",
|
||||
"@types/ldapjs": "^1.0.10",
|
||||
"@types/node": "^14.14.43",
|
||||
"@types/proper-lockfile": "^4.1.1",
|
||||
"clean-webpack-plugin": "^3.0.0",
|
||||
"concurrently": "^6.0.2",
|
||||
"copy-webpack-plugin": "^6.4.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "^5.2.4",
|
||||
"del": "^6.0.0",
|
||||
"electron-builder": "^22.11.7",
|
||||
"electron-notarize": "^1.1.1",
|
||||
"electron-rebuild": "^3.2.3",
|
||||
"electron-reload": "^1.5.0",
|
||||
"file-loader": "^6.2.0",
|
||||
"font-awesome": "4.7.0",
|
||||
"google-fonts-webpack-plugin": "^0.4.4",
|
||||
"html-loader": "^0.5.1",
|
||||
"html-webpack-plugin": "^2.30.1",
|
||||
"node-loader": "^0.6.0",
|
||||
"node-sass": "^4.7.2",
|
||||
"rimraf": "^2.6.2",
|
||||
"sass-loader": "^6.0.6",
|
||||
"ts-loader": "^3.5.0",
|
||||
"tslint": "^5.9.1",
|
||||
"tslint-loader": "^3.5.3",
|
||||
"typescript": "^2.7.1",
|
||||
"webpack": "^3.10.0",
|
||||
"webpack-merge": "^4.1.0",
|
||||
"webpack-node-externals": "^1.6.0"
|
||||
"html-loader": "^1.3.2",
|
||||
"html-webpack-plugin": "^4.5.1",
|
||||
"mini-css-extract-plugin": "^1.5.0",
|
||||
"node-loader": "^1.0.3",
|
||||
"pkg": "^5.1.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"sass": "^1.32.11",
|
||||
"sass-loader": "^10.1.1",
|
||||
"tapable": "^1.1.3",
|
||||
"ts-loader": "^8.1.0",
|
||||
"tsconfig-paths-webpack-plugin": "^3.5.1",
|
||||
"tslint": "~6.1.0",
|
||||
"tslint-loader": "^3.5.4",
|
||||
"typescript": "4.1.5",
|
||||
"webpack": "^4.46.0",
|
||||
"webpack-cli": "^4.6.0",
|
||||
"webpack-merge": "^5.7.3",
|
||||
"webpack-node-externals": "^3.0.0",
|
||||
"prebuild-install": "^5.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/animations": "5.2.0",
|
||||
"@angular/common": "5.2.0",
|
||||
"@angular/compiler": "5.2.0",
|
||||
"@angular/core": "5.2.0",
|
||||
"@angular/forms": "5.2.0",
|
||||
"@angular/http": "5.2.0",
|
||||
"@angular/platform-browser": "5.2.0",
|
||||
"@angular/platform-browser-dynamic": "5.2.0",
|
||||
"@angular/router": "5.2.0",
|
||||
"@angular/upgrade": "5.2.0",
|
||||
"@microsoft/microsoft-graph-client": "1.0.0",
|
||||
"@okta/okta-sdk-nodejs": "1.1.0",
|
||||
"angular2-toaster": "4.0.2",
|
||||
"angulartics2": "5.0.1",
|
||||
"bootstrap": "4.1.0",
|
||||
"core-js": "2.4.1",
|
||||
"electron-log": "2.2.14",
|
||||
"electron-updater": "2.21.4",
|
||||
"googleapis": "29.0.0",
|
||||
"keytar": "4.1.0",
|
||||
"ldapjs": "1.0.2",
|
||||
"lowdb": "1.0.0",
|
||||
"lunr": "2.1.6",
|
||||
"node-forge": "0.7.1",
|
||||
"rxjs": "5.5.6",
|
||||
"zone.js": "0.8.19"
|
||||
"@bitwarden/jslib-angular": "file:jslib/angular",
|
||||
"@bitwarden/jslib-common": "file:jslib/common",
|
||||
"@bitwarden/jslib-electron": "file:jslib/electron",
|
||||
"@bitwarden/jslib-node": "file:jslib/node",
|
||||
"@microsoft/microsoft-graph-client": "^2.2.1",
|
||||
"angular2-toaster": "^11.0.1",
|
||||
"bootstrap": "^4.6.0",
|
||||
"chalk": "^4.1.1",
|
||||
"commander": "^7.2.0",
|
||||
"core-js": "^3.11.0",
|
||||
"duo_web_sdk": "git+https://github.com/duosecurity/duo_web_sdk.git",
|
||||
"form-data": "^4.0.0",
|
||||
"googleapis": "^73.0.0",
|
||||
"inquirer": "8.0.0",
|
||||
"ldapjs": "git+https://git@github.com/kspearrin/node-ldapjs.git",
|
||||
"lunr": "^2.3.9",
|
||||
"open": "^8.0.6",
|
||||
"proper-lockfile": "^4.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "~14",
|
||||
"npm": "~7"
|
||||
}
|
||||
}
|
||||
|
||||
10
resources/entitlements.mac.plist
Normal file
10
resources/entitlements.mac.plist
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.disable-library-validation</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
18
scripts/notarize.js
Normal file
18
scripts/notarize.js
Normal file
@@ -0,0 +1,18 @@
|
||||
require('dotenv').config();
|
||||
const { notarize } = require('electron-notarize');
|
||||
|
||||
exports.default = async function notarizing(context) {
|
||||
const { electronPlatformName, appOutDir } = context;
|
||||
if (electronPlatformName !== 'darwin') {
|
||||
return;
|
||||
}
|
||||
const appleId = process.env.APPLE_ID_USERNAME || process.env.APPLEID;
|
||||
const appleIdPassword = process.env.APPLE_ID_PASSWORD || `@keychain:AC_PASSWORD`;
|
||||
const appName = context.packager.appInfo.productFilename;
|
||||
return await notarize({
|
||||
appBundleId: 'com.bitwarden.directory-connector',
|
||||
appPath: `${appOutDir}/${appName}.app`,
|
||||
appleId: appleId,
|
||||
appleIdPassword: appleIdPassword,
|
||||
});
|
||||
};
|
||||
23
scripts/sign.js
Normal file
23
scripts/sign.js
Normal file
@@ -0,0 +1,23 @@
|
||||
exports.default = async function(configuration) {
|
||||
if (
|
||||
parseInt(process.env.ELECTRON_BUILDER_SIGN) === 1 &&
|
||||
configuration.path.slice(-4) == ".exe"
|
||||
) {
|
||||
console.log(`[*] Signing file: ${configuration.path}`)
|
||||
require("child_process").execSync(
|
||||
`azuresigntool sign ` +
|
||||
`-kvu ${process.env.SIGNING_VAULT_URL} ` +
|
||||
`-kvi ${process.env.SIGNING_CLIENT_ID} ` +
|
||||
`-kvt ${process.env.SIGNING_TENANT_ID} ` +
|
||||
`-kvs ${process.env.SIGNING_CLIENT_SECRET} ` +
|
||||
`-kvc ${process.env.SIGNING_CERT_NAME} ` +
|
||||
`-fd ${configuration.hash} ` +
|
||||
`-du ${configuration.site} ` +
|
||||
`-tr http://timestamp.digicert.com ` +
|
||||
`"${configuration.path}"`,
|
||||
{
|
||||
stdio: "inherit"
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
24
src-cli/package.json
Normal file
24
src-cli/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "bitwarden-directory-connector",
|
||||
"productName": "Bitwarden Directory Connector",
|
||||
"description": "Sync your user directory to your Bitwarden organization.",
|
||||
"version": "2.9.5",
|
||||
"author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)",
|
||||
"homepage": "https://bitwarden.com",
|
||||
"license": "GPL-3.0",
|
||||
"main": "main.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/bitwarden/directory-connector"
|
||||
},
|
||||
"bin": {
|
||||
"bwdc": "../build-cli/bwdc.js"
|
||||
},
|
||||
"pkg": {
|
||||
"assets": "../build-cli/**/*"
|
||||
},
|
||||
"dependencies": {
|
||||
"browser-hrtime": "^1.1.8",
|
||||
"keytar": "7.6.0"
|
||||
}
|
||||
}
|
||||
47
src/app/accounts/apiKey.component.html
Normal file
47
src/app/accounts/apiKey.component.html
Normal file
@@ -0,0 +1,47 @@
|
||||
<div class="container-fluid">
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8 col-lg-6">
|
||||
<p class="text-center font-weight-bold">{{'welcome' | i18n}}</p>
|
||||
<p class="text-center">{{'logInDesc' | i18n}}</p>
|
||||
<div class="card">
|
||||
<h5 class="card-header">{{'logIn' | i18n}}</h5>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label for="client_id">{{'clientId' | i18n}}</label>
|
||||
<input id="client_id" name="ClientId" [(ngModel)]="clientId"
|
||||
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>
|
||||
</form>
|
||||
</div>
|
||||
<ng-template #environment></ng-template>
|
||||
84
src/app/accounts/apiKey.component.ts
Normal file
84
src/app/accounts/apiKey.component.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import {
|
||||
Component,
|
||||
ComponentFactoryResolver,
|
||||
Input,
|
||||
ViewChild,
|
||||
ViewContainerRef,
|
||||
} from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { EnvironmentComponent } from './environment.component';
|
||||
|
||||
import { ApiKeyService } from 'jslib-common/abstractions/apiKey.service';
|
||||
import { AuthService } from 'jslib-common/abstractions/auth.service';
|
||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
||||
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
|
||||
|
||||
import { ModalService } from 'jslib-angular/services/modal.service';
|
||||
|
||||
import { Utils } from 'jslib-common/misc/utils';
|
||||
import { ConfigurationService } from '../../services/configuration.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-apiKey',
|
||||
templateUrl: 'apiKey.component.html',
|
||||
})
|
||||
export class ApiKeyComponent {
|
||||
@ViewChild('environment', { read: ViewContainerRef, static: true }) environmentModal: ViewContainerRef;
|
||||
@Input() clientId: string = '';
|
||||
@Input() clientSecret: string = '';
|
||||
|
||||
formPromise: Promise<any>;
|
||||
successRoute = '/tabs/dashboard';
|
||||
showSecret: boolean = false;
|
||||
|
||||
constructor(private authService: AuthService, private apiKeyService: ApiKeyService, private router: Router,
|
||||
private i18nService: I18nService, private componentFactoryResolver: ComponentFactoryResolver,
|
||||
private configurationService: ConfigurationService, private platformUtilsService: PlatformUtilsService,
|
||||
private modalService: ModalService) { }
|
||||
|
||||
async submit() {
|
||||
if (this.clientId == null || this.clientId === '') {
|
||||
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
|
||||
this.i18nService.t('clientIdRequired'));
|
||||
return;
|
||||
}
|
||||
if (!this.clientId.startsWith('organization')) {
|
||||
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
|
||||
this.i18nService.t('orgApiKeyRequired'));
|
||||
return;
|
||||
}
|
||||
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])) {
|
||||
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
|
||||
this.i18nService.t('invalidClientId'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
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() {
|
||||
const [modalRef, childComponent] = await this.modalService.openViewRef(EnvironmentComponent, this.environmentModal);
|
||||
|
||||
childComponent.onSaved.subscribe(() => {
|
||||
modalRef.close();
|
||||
});
|
||||
}
|
||||
toggleSecret() {
|
||||
this.showSecret = !this.showSecret;
|
||||
document.getElementById('client_secret').focus();
|
||||
}
|
||||
}
|
||||
@@ -17,13 +17,19 @@
|
||||
</div>
|
||||
<h4>{{'customEnvironment' | i18n}}</h4>
|
||||
<p>{{'customEnvironmentFooter' | i18n}}</p>
|
||||
<div class="form-group">
|
||||
<label for="webVaultUrl">{{'webVaultUrl' | i18n}}</label>
|
||||
<input id="webVaultUrl" type="text" name="WebVaultUrl" [(ngModel)]="webVaultUrl"
|
||||
class="form-control">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="apiUrl">{{'apiUrl' | i18n}}</label>
|
||||
<input id="apiUrl" type="text" name="ApiUrl" [(ngModel)]="apiUrl" class="form-control">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="identityUrl">{{'identityUrl' | i18n}}</label>
|
||||
<input id="identityUrl" type="text" name="IdentityUrl" [(ngModel)]="identityUrl" class="form-control">
|
||||
<input id="identityUrl" type="text" name="IdentityUrl" [(ngModel)]="identityUrl"
|
||||
class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer justify-content-start">
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
import { EnvironmentService } from 'jslib-common/abstractions/environment.service';
|
||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
||||
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
|
||||
|
||||
import { EnvironmentService } from 'jslib/abstractions/environment.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
|
||||
import { EnvironmentComponent as BaseEnvironmentComponent } from 'jslib/angular/components/environment.component';
|
||||
import { EnvironmentComponent as BaseEnvironmentComponent } from 'jslib-angular/components/environment.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-environment',
|
||||
templateUrl: 'environment.component.html',
|
||||
})
|
||||
export class EnvironmentComponent extends BaseEnvironmentComponent {
|
||||
constructor(analytics: Angulartics2, toasterService: ToasterService,
|
||||
environmentService: EnvironmentService, i18nService: I18nService) {
|
||||
super(analytics, toasterService, environmentService, i18nService);
|
||||
constructor(environmentService: EnvironmentService, i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService) {
|
||||
super(platformUtilsService, environmentService, i18nService);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
<div class="container-fluid">
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8 col-lg-6">
|
||||
<p class="text-center font-weight-bold">{{'welcome' | i18n}}</p>
|
||||
<p class="text-center">{{'logInDesc' | i18n}}</p>
|
||||
<div class="card">
|
||||
<h5 class="card-header">{{'logIn' | i18n}}</h5>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label for="email">{{'emailAddress' | i18n}}</label>
|
||||
<input id="email" type="text" name="Email" [(ngModel)]="email" class="form-control" appAutoFocus>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="row-main">
|
||||
<label for="masterPassword">{{'masterPass' | i18n}}</label>
|
||||
<input id="masterPassword" type="password" name="MasterPassword" [(ngModel)]="masterPassword" class="form-control">
|
||||
</div>
|
||||
</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>
|
||||
<button type="button" class="btn btn-link" (click)="settings()">
|
||||
{{'settings' | i18n}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<ng-template #environment></ng-template>
|
||||
@@ -1,45 +0,0 @@
|
||||
import {
|
||||
Component,
|
||||
ComponentFactoryResolver,
|
||||
ViewChild,
|
||||
ViewContainerRef,
|
||||
} from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { EnvironmentComponent } from './environment.component';
|
||||
|
||||
import { AuthService } from 'jslib/abstractions/auth.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { SyncService } from 'jslib/abstractions/sync.service';
|
||||
|
||||
import { LoginComponent as BaseLoginComponent } from 'jslib/angular/components/login.component';
|
||||
import { ModalComponent } from 'jslib/angular/components/modal.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-login',
|
||||
templateUrl: 'login.component.html',
|
||||
})
|
||||
export class LoginComponent extends BaseLoginComponent {
|
||||
@ViewChild('environment', { read: ViewContainerRef }) environmentModal: ViewContainerRef;
|
||||
|
||||
constructor(authService: AuthService, router: Router,
|
||||
analytics: Angulartics2, toasterService: ToasterService,
|
||||
i18nService: I18nService, private componentFactoryResolver: ComponentFactoryResolver) {
|
||||
super(authService, router, analytics, toasterService, i18nService);
|
||||
super.successRoute = '/tabs/dashboard';
|
||||
}
|
||||
|
||||
settings() {
|
||||
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
|
||||
const modal = this.environmentModal.createComponent(factory).instance;
|
||||
const childComponent = modal.show<EnvironmentComponent>(EnvironmentComponent,
|
||||
this.environmentModal);
|
||||
|
||||
childComponent.onSaved.subscribe(() => {
|
||||
modal.close();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
<div class="modal fade">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title">{{'twoStepOptions' | i18n}}</h3>
|
||||
<button type="button" class="close" data-dismiss="modal" title="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p *ngFor="let p of providers">
|
||||
<a href="#" (click)="choose(p)">
|
||||
<strong>{{p.name}}</strong>
|
||||
</a>
|
||||
<br /> {{p.description}}
|
||||
</p>
|
||||
<p>
|
||||
<a href="#" (click)="recover()">
|
||||
<strong>{{'recoveryCodeTitle' | i18n}}</strong>
|
||||
</a>
|
||||
<br /> {{'recoveryCodeDesc' | i18n}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,25 +0,0 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { AuthService } from 'jslib/abstractions/auth.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
|
||||
import {
|
||||
TwoFactorOptionsComponent as BaseTwoFactorOptionsComponent,
|
||||
} from 'jslib/angular/components/two-factor-options.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-two-factor-options',
|
||||
templateUrl: 'two-factor-options.component.html',
|
||||
})
|
||||
export class TwoFactorOptionsComponent extends BaseTwoFactorOptionsComponent {
|
||||
constructor(authService: AuthService, router: Router,
|
||||
analytics: Angulartics2, toasterService: ToasterService,
|
||||
i18nService: I18nService, platformUtilsService: PlatformUtilsService) {
|
||||
super(authService, router, analytics, toasterService, i18nService, platformUtilsService, window);
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
<div class="container-fluid">
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8 col-lg-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">{{title}}</h5>
|
||||
<div class="card-body">
|
||||
<ng-container *ngIf="selectedProviderType === providerType.Email || selectedProviderType === providerType.Authenticator">
|
||||
<p *ngIf="selectedProviderType === providerType.Authenticator">
|
||||
{{'enterVerificationCodeApp' | i18n}}
|
||||
</p>
|
||||
<p *ngIf="selectedProviderType === providerType.Email">
|
||||
{{'enterVerificationCodeEmail' | i18n : twoFactorEmail}}
|
||||
</p>
|
||||
<div class="form-group">
|
||||
<label for="code">{{'verificationCode' | i18n}}</label>
|
||||
<input id="code" type="text" name="Code" [(ngModel)]="token" appAutofocus class="form-control">
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="selectedProviderType === providerType.Yubikey">
|
||||
<p>{{'insertYubiKey' | i18n}}</p>
|
||||
<img src="../../images/yubikey.jpg" alt="">
|
||||
<div class="form-group">
|
||||
<label for="code">{{'verificationCode' | i18n}}</label>
|
||||
<input id="code" type="password" name="Code" [(ngModel)]="token" appAutofocus class="form-control">
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="selectedProviderType === providerType.Duo ||
|
||||
selectedProviderType === providerType.OrganizationDuo">
|
||||
<div id="duo-frame">
|
||||
<iframe id="duo_iframe"></iframe>
|
||||
</div>
|
||||
</ng-container>
|
||||
<div class="form-group" *ngIf="selectedProviderType !== null && selectedProviderType !== providerType.U2f">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="remember" [(ngModel)]="remember" name="Remember">
|
||||
<label class="form-check-label" for="remember">{{'rememberMe' | i18n}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<ng-container class="card-body" *ngIf="selectedProviderType === null || selectedProviderType === providerType.U2f">
|
||||
<p>{{'noTwoStepProviders' | i18n}}</p>
|
||||
<p>{{'noTwoStepProviders2' | i18n}}</p>
|
||||
</ng-container>
|
||||
<button type="submit" class="btn btn-primary" [disabled]="form.loading" *ngIf="selectedProviderType != null && selectedProviderType !== providerType.U2f && selectedProviderType !== providerType.Duo &&
|
||||
selectedProviderType !== providerType.OrganizationDuo">
|
||||
<i class="fa fa-sign-in fa-fw" [hidden]="form.loading"></i>
|
||||
<i class="fa fa-spinner fa-fw fa-spin" [hidden]="!form.loading"></i>
|
||||
{{'continue' | i18n}}
|
||||
</button>
|
||||
<a routerLink="/login" class="btn btn-link">{{'cancel' | i18n}}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center mt-3">
|
||||
<a href="#" appStopClick (click)="anotherMethod()">{{'useAnotherTwoStepMethod' | i18n}}</a>
|
||||
<a href="#" appStopClick (click)="sendEmail(true)" [appApiAction]="emailPromise" *ngIf="selectedProviderType === providerType.Email">
|
||||
{{'sendVerificationCodeEmailAgain' | i18n}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<ng-template #twoFactorOptions></ng-template>
|
||||
@@ -1,59 +0,0 @@
|
||||
import {
|
||||
Component,
|
||||
ComponentFactoryResolver,
|
||||
ViewChild,
|
||||
ViewContainerRef,
|
||||
} from '@angular/core';
|
||||
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { TwoFactorOptionsComponent } from './two-factor-options.component';
|
||||
|
||||
import { TwoFactorProviderType } from 'jslib/enums/twoFactorProviderType';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { AuthService } from 'jslib/abstractions/auth.service';
|
||||
import { EnvironmentService } from 'jslib/abstractions/environment.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { SyncService } from 'jslib/abstractions/sync.service';
|
||||
|
||||
import { ModalComponent } from 'jslib/angular/components/modal.component';
|
||||
import { TwoFactorComponent as BaseTwoFactorComponent } from 'jslib/angular/components/two-factor.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-two-factor',
|
||||
templateUrl: 'two-factor.component.html',
|
||||
})
|
||||
export class TwoFactorComponent extends BaseTwoFactorComponent {
|
||||
@ViewChild('twoFactorOptions', { read: ViewContainerRef }) twoFactorOptionsModal: ViewContainerRef;
|
||||
|
||||
constructor(authService: AuthService, router: Router,
|
||||
analytics: Angulartics2, toasterService: ToasterService,
|
||||
i18nService: I18nService, apiService: ApiService,
|
||||
platformUtilsService: PlatformUtilsService, environmentService: EnvironmentService,
|
||||
private componentFactoryResolver: ComponentFactoryResolver) {
|
||||
super(authService, router, analytics, toasterService, i18nService, apiService,
|
||||
platformUtilsService, window, environmentService);
|
||||
super.successRoute = '/tabs/dashboard';
|
||||
}
|
||||
|
||||
anotherMethod() {
|
||||
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
|
||||
const modal = this.twoFactorOptionsModal.createComponent(factory).instance;
|
||||
const childComponent = modal.show<TwoFactorOptionsComponent>(TwoFactorOptionsComponent,
|
||||
this.twoFactorOptionsModal);
|
||||
|
||||
childComponent.onProviderSelected.subscribe(async (provider: TwoFactorProviderType) => {
|
||||
modal.close();
|
||||
this.selectedProviderType = provider;
|
||||
await this.init();
|
||||
});
|
||||
childComponent.onRecoverSelected.subscribe(() => {
|
||||
modal.close();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,7 @@ import {
|
||||
import { AuthGuardService } from './services/auth-guard.service';
|
||||
import { LaunchGuardService } from './services/launch-guard.service';
|
||||
|
||||
import { LoginComponent } from './accounts/login.component';
|
||||
import { TwoFactorComponent } from './accounts/two-factor.component';
|
||||
import { ApiKeyComponent } from './accounts/apiKey.component';
|
||||
import { DashboardComponent } from './tabs/dashboard.component';
|
||||
import { MoreComponent } from './tabs/more.component';
|
||||
import { SettingsComponent } from './tabs/settings.component';
|
||||
@@ -18,10 +17,9 @@ const routes: Routes = [
|
||||
{ path: '', redirectTo: '/login', pathMatch: 'full' },
|
||||
{
|
||||
path: 'login',
|
||||
component: LoginComponent,
|
||||
component: ApiKeyComponent,
|
||||
canActivate: [LaunchGuardService],
|
||||
},
|
||||
{ path: '2fa', component: TwoFactorComponent },
|
||||
{
|
||||
path: 'tabs',
|
||||
component: TabsComponent,
|
||||
|
||||
@@ -1,39 +1,34 @@
|
||||
import {
|
||||
BodyOutputType,
|
||||
Toast,
|
||||
ToasterConfig,
|
||||
ToasterContainerComponent,
|
||||
ToasterService,
|
||||
} from 'angular2-toaster';
|
||||
import { Angulartics2GoogleAnalytics } from 'angulartics2/ga';
|
||||
|
||||
import {
|
||||
Component,
|
||||
ComponentFactoryResolver,
|
||||
NgZone,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
SecurityContext,
|
||||
Type,
|
||||
ViewChild,
|
||||
ViewContainerRef,
|
||||
} from '@angular/core';
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
import { BroadcasterService } from 'jslib-angular/services/broadcaster.service';
|
||||
|
||||
import { ModalComponent } from 'jslib/angular/components/modal.component';
|
||||
|
||||
import { BroadcasterService } from 'jslib/angular/services/broadcaster.service';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { AuthService } from 'jslib/abstractions/auth.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { MessagingService } from 'jslib/abstractions/messaging.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { StateService } from 'jslib/abstractions/state.service';
|
||||
import { StorageService } from 'jslib/abstractions/storage.service';
|
||||
import { TokenService } from 'jslib/abstractions/token.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { ConstantsService } from 'jslib/services/constants.service';
|
||||
import { ApiService } from 'jslib-common/abstractions/api.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';
|
||||
import { SyncService } from '../services/sync.service';
|
||||
@@ -49,7 +44,7 @@ const BroadcasterSubscriptionId = 'AppComponent';
|
||||
<router-outlet></router-outlet>`,
|
||||
})
|
||||
export class AppComponent implements OnInit {
|
||||
@ViewChild('settings', { read: ViewContainerRef }) settingsRef: ViewContainerRef;
|
||||
@ViewChild('settings', { read: ViewContainerRef, static: true }) settingsRef: ViewContainerRef;
|
||||
|
||||
toasterConfig: ToasterConfig = new ToasterConfig({
|
||||
showCloseButton: true,
|
||||
@@ -59,30 +54,22 @@ export class AppComponent implements OnInit {
|
||||
});
|
||||
|
||||
private lastActivity: number = null;
|
||||
private modal: ModalComponent = null;
|
||||
|
||||
constructor(private angulartics2GoogleAnalytics: Angulartics2GoogleAnalytics,
|
||||
private broadcasterService: BroadcasterService, private userService: UserService,
|
||||
constructor(private broadcasterService: BroadcasterService, private userService: UserService,
|
||||
private tokenService: TokenService, private storageService: StorageService,
|
||||
private authService: AuthService, private router: Router, private analytics: Angulartics2,
|
||||
private authService: AuthService, private router: Router,
|
||||
private toasterService: ToasterService, private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService, private ngZone: NgZone,
|
||||
private sanitizer: DomSanitizer, private ngZone: NgZone,
|
||||
private componentFactoryResolver: ComponentFactoryResolver, private messagingService: MessagingService,
|
||||
private configurationService: ConfigurationService, private syncService: SyncService,
|
||||
private stateService: StateService, private apiService: ApiService) {
|
||||
(window as any).BitwardenToasterService = toasterService;
|
||||
}
|
||||
(window as any).BitwardenToasterService = toasterService;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
|
||||
this.ngZone.run(async () => {
|
||||
switch (message.command) {
|
||||
case 'loggedIn':
|
||||
if (await this.userService.isAuthenticated()) {
|
||||
const profile = await this.apiService.getProfile();
|
||||
this.stateService.save('profileOrganizations', profile.organizations);
|
||||
}
|
||||
break;
|
||||
case 'syncScheduleStarted':
|
||||
case 'syncScheduleStopped':
|
||||
this.stateService.save('syncingDir', message.command === 'syncScheduleStarted');
|
||||
@@ -125,6 +112,12 @@ export class AppComponent implements OnInit {
|
||||
|
||||
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:
|
||||
}
|
||||
});
|
||||
@@ -138,13 +131,10 @@ export class AppComponent implements OnInit {
|
||||
private async logOut(expired: boolean) {
|
||||
const userId = await this.userService.getUserId();
|
||||
|
||||
await Promise.all([
|
||||
this.tokenService.clearToken(),
|
||||
this.userService.clear(),
|
||||
]);
|
||||
await this.tokenService.clearToken();
|
||||
await this.userService.clear();
|
||||
|
||||
this.authService.logOut(async () => {
|
||||
this.analytics.eventTrack.next({ action: 'Logged Out' });
|
||||
if (expired) {
|
||||
this.toasterService.popAsync('warning', this.i18nService.t('loggedOut'),
|
||||
this.i18nService.t('loginExpired'));
|
||||
@@ -153,17 +143,30 @@ export class AppComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
private openModal<T>(type: Type<T>, ref: ViewContainerRef) {
|
||||
if (this.modal != null) {
|
||||
this.modal.close();
|
||||
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;
|
||||
}
|
||||
|
||||
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
|
||||
this.modal = ref.createComponent(factory).instance;
|
||||
const childComponent = this.modal.show<T>(type, ref);
|
||||
|
||||
this.modal.onClosed.subscribe(() => {
|
||||
this.modal = null;
|
||||
});
|
||||
if (msg.options != null) {
|
||||
if (msg.options.trustedHtml === true) {
|
||||
toast.bodyOutputType = BodyOutputType.TrustedHtml;
|
||||
}
|
||||
if (msg.options.timeout != null && msg.options.timeout > 0) {
|
||||
toast.timeout = msg.options.timeout;
|
||||
}
|
||||
}
|
||||
this.toasterService.popAsync(toast);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import 'core-js';
|
||||
import 'core-js/stable';
|
||||
import 'zone.js/dist/zone';
|
||||
|
||||
import { ToasterModule } from 'angular2-toaster';
|
||||
import { Angulartics2Module } from 'angulartics2';
|
||||
import { Angulartics2GoogleAnalytics } from 'angulartics2/ga';
|
||||
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { ServicesModule } from './services/services.module';
|
||||
@@ -15,28 +13,27 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
|
||||
import { AppComponent } from './app.component';
|
||||
|
||||
import { IconComponent } from 'jslib/angular/components/icon.component';
|
||||
import { ModalComponent } from 'jslib/angular/components/modal.component';
|
||||
import { CalloutComponent } from 'jslib-angular/components/callout.component';
|
||||
import { IconComponent } from 'jslib-angular/components/icon.component';
|
||||
|
||||
import { ApiKeyComponent } from './accounts/apiKey.component';
|
||||
import { EnvironmentComponent } from './accounts/environment.component';
|
||||
import { LoginComponent } from './accounts/login.component';
|
||||
import { TwoFactorOptionsComponent } from './accounts/two-factor-options.component';
|
||||
import { TwoFactorComponent } from './accounts/two-factor.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 { ApiActionDirective } from 'jslib/angular/directives/api-action.directive';
|
||||
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 { A11yTitleDirective } from 'jslib-angular/directives/a11y-title.directive';
|
||||
import { ApiActionDirective } from 'jslib-angular/directives/api-action.directive';
|
||||
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';
|
||||
import { I18nPipe } from 'jslib-angular/pipes/i18n.pipe';
|
||||
import { SearchCiphersPipe } from 'jslib-angular/pipes/search-ciphers.pipe';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -45,39 +42,28 @@ import { SearchCiphersPipe } from 'jslib/angular/pipes/search-ciphers.pipe';
|
||||
FormsModule,
|
||||
AppRoutingModule,
|
||||
ServicesModule,
|
||||
Angulartics2Module.forRoot([Angulartics2GoogleAnalytics], {
|
||||
pageTracking: {
|
||||
clearQueryParams: true,
|
||||
},
|
||||
}),
|
||||
ToasterModule,
|
||||
ToasterModule.forRoot(),
|
||||
],
|
||||
declarations: [
|
||||
A11yTitleDirective,
|
||||
ApiActionDirective,
|
||||
ApiKeyComponent,
|
||||
AppComponent,
|
||||
AutofocusDirective,
|
||||
BlurClickDirective,
|
||||
BoxRowDirective,
|
||||
CalloutComponent,
|
||||
DashboardComponent,
|
||||
EnvironmentComponent,
|
||||
FallbackSrcDirective,
|
||||
I18nPipe,
|
||||
IconComponent,
|
||||
LoginComponent,
|
||||
ModalComponent,
|
||||
MoreComponent,
|
||||
SearchCiphersPipe,
|
||||
SettingsComponent,
|
||||
StopClickDirective,
|
||||
StopPropDirective,
|
||||
TabsComponent,
|
||||
TwoFactorComponent,
|
||||
TwoFactorOptionsComponent,
|
||||
],
|
||||
entryComponents: [
|
||||
EnvironmentComponent,
|
||||
ModalComponent,
|
||||
TwoFactorOptionsComponent,
|
||||
],
|
||||
providers: [],
|
||||
bootstrap: [AppComponent],
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { enableProdMode } from '@angular/core';
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||
|
||||
import { isDev } from 'jslib/electron/utils';
|
||||
import { isDev } from 'jslib-electron/utils';
|
||||
|
||||
// tslint:disable-next-line
|
||||
require('../scss/styles.scss');
|
||||
// tslint:disable-next-line
|
||||
require('../../jslib/src/misc/duo.js');
|
||||
|
||||
import { AppModule } from './app.module';
|
||||
|
||||
@@ -14,4 +12,4 @@ if (!isDev()) {
|
||||
enableProdMode();
|
||||
}
|
||||
|
||||
platformBrowserDynamic().bootstrapModule(AppModule);
|
||||
platformBrowserDynamic().bootstrapModule(AppModule, { preserveWhitespaces: true });
|
||||
|
||||
@@ -3,17 +3,17 @@ import {
|
||||
CanActivate,
|
||||
Router,
|
||||
} from '@angular/router';
|
||||
import { ApiKeyService } from 'jslib-common/abstractions/apiKey.service';
|
||||
|
||||
import { MessagingService } from 'jslib/abstractions/messaging.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
import { MessagingService } from 'jslib-common/abstractions/messaging.service';
|
||||
|
||||
@Injectable()
|
||||
export class AuthGuardService implements CanActivate {
|
||||
constructor(private userService: UserService, private router: Router,
|
||||
constructor(private apiKeyService: ApiKeyService, private router: Router,
|
||||
private messagingService: MessagingService) { }
|
||||
|
||||
async canActivate() {
|
||||
const isAuthed = await this.userService.isAuthenticated();
|
||||
const isAuthed = await this.apiKeyService.isAuthenticated();
|
||||
if (!isAuthed) {
|
||||
this.messagingService.send('logout');
|
||||
return false;
|
||||
|
||||
@@ -4,14 +4,14 @@ import {
|
||||
Router,
|
||||
} from '@angular/router';
|
||||
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
import { ApiKeyService } from 'jslib-common/abstractions/apiKey.service';
|
||||
|
||||
@Injectable()
|
||||
export class LaunchGuardService implements CanActivate {
|
||||
constructor(private userService: UserService, private router: Router) { }
|
||||
constructor(private apiKeyService: ApiKeyService, private router: Router) { }
|
||||
|
||||
async canActivate() {
|
||||
const isAuthed = await this.userService.isAuthenticated();
|
||||
const isAuthed = await this.apiKeyService.isAuthenticated();
|
||||
if (!isAuthed) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { remote } from 'electron';
|
||||
|
||||
import {
|
||||
APP_INITIALIZER,
|
||||
NgModule,
|
||||
@@ -7,11 +5,11 @@ import {
|
||||
|
||||
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 { isDev } from 'jslib/electron/utils';
|
||||
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 { AuthGuardService } from './auth-guard.service';
|
||||
import { LaunchGuardService } from './launch-guard.service';
|
||||
@@ -20,72 +18,84 @@ import { ConfigurationService } from '../../services/configuration.service';
|
||||
import { I18nService } from '../../services/i18n.service';
|
||||
import { SyncService } from '../../services/sync.service';
|
||||
|
||||
import { BroadcasterService } from 'jslib/angular/services/broadcaster.service';
|
||||
import { ValidationService } from 'jslib/angular/services/validation.service';
|
||||
import { BroadcasterService } from 'jslib-angular/services/broadcaster.service';
|
||||
import { ModalService } from 'jslib-angular/services/modal.service';
|
||||
import { ValidationService } from 'jslib-angular/services/validation.service';
|
||||
|
||||
import { Analytics } from 'jslib/misc/analytics';
|
||||
import { ApiKeyService } from 'jslib-common/services/apiKey.service';
|
||||
import { AppIdService } from 'jslib-common/services/appId.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 { ApiService } from 'jslib/services/api.service';
|
||||
import { AppIdService } from 'jslib/services/appId.service';
|
||||
import { AuthService } from 'jslib/services/auth.service';
|
||||
import { ConstantsService } from 'jslib/services/constants.service';
|
||||
import { ContainerService } from 'jslib/services/container.service';
|
||||
import { CryptoService } from 'jslib/services/crypto.service';
|
||||
import { EnvironmentService } from 'jslib/services/environment.service';
|
||||
import { LowdbStorageService } from 'jslib/services/lowdbStorage.service';
|
||||
import { NodeCryptoFunctionService } from 'jslib/services/nodeCryptoFunction.service';
|
||||
import { StateService } from 'jslib/services/state.service';
|
||||
import { TokenService } from 'jslib/services/token.service';
|
||||
import { UserService } from 'jslib/services/user.service';
|
||||
import { NodeCryptoFunctionService } from 'jslib-node/services/nodeCryptoFunction.service';
|
||||
|
||||
import { ApiService as ApiServiceAbstraction } from 'jslib/abstractions/api.service';
|
||||
import { AppIdService as AppIdServiceAbstraction } from 'jslib/abstractions/appId.service';
|
||||
import { AuthService as AuthServiceAbstraction } from 'jslib/abstractions/auth.service';
|
||||
import { CryptoService as CryptoServiceAbstraction } from 'jslib/abstractions/crypto.service';
|
||||
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from 'jslib/abstractions/cryptoFunction.service';
|
||||
import { EnvironmentService as EnvironmentServiceAbstraction } from 'jslib/abstractions/environment.service';
|
||||
import { I18nService as I18nServiceAbstraction } from 'jslib/abstractions/i18n.service';
|
||||
import { LogService as LogServiceAbstraction } from 'jslib/abstractions/log.service';
|
||||
import { MessagingService as MessagingServiceAbstraction } from 'jslib/abstractions/messaging.service';
|
||||
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from 'jslib/abstractions/platformUtils.service';
|
||||
import { StateService as StateServiceAbstraction } from 'jslib/abstractions/state.service';
|
||||
import { StorageService as StorageServiceAbstraction } from 'jslib/abstractions/storage.service';
|
||||
import { TokenService as TokenServiceAbstraction } from 'jslib/abstractions/token.service';
|
||||
import { UserService as UserServiceAbstraction } from 'jslib/abstractions/user.service';
|
||||
import { ApiService as ApiServiceAbstraction } from 'jslib-common/abstractions/api.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 { AuthService } from '../../services/auth.service';
|
||||
|
||||
const logService = new ElectronLogService();
|
||||
const i18nService = new I18nService(window.navigator.language, './locales');
|
||||
const stateService = new StateService();
|
||||
const platformUtilsService = new ElectronPlatformUtilsService(i18nService, true);
|
||||
const broadcasterService = new BroadcasterService();
|
||||
const messagingService = new ElectronRendererMessagingService(broadcasterService);
|
||||
const storageService: StorageServiceAbstraction = new LowdbStorageService(null, remote.app.getPath('userData'));
|
||||
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);
|
||||
const cryptoService = new CryptoService(storageService, secureStorageService, cryptoFunctionService,
|
||||
platformUtilsService, logService);
|
||||
const appIdService = new AppIdService(storageService);
|
||||
const tokenService = new TokenService(storageService);
|
||||
const apiService = new ApiService(tokenService, platformUtilsService,
|
||||
const environmentService = new EnvironmentService(storageService);
|
||||
const apiService = new ApiService(tokenService, platformUtilsService, environmentService, refreshTokenCallback,
|
||||
async (expired: boolean) => messagingService.send('logout', { expired: expired }));
|
||||
const environmentService = new EnvironmentService(apiService, storageService);
|
||||
const userService = new UserService(tokenService, storageService);
|
||||
const containerService = new ContainerService(cryptoService, platformUtilsService);
|
||||
const apiKeyService = new ApiKeyService(tokenService, storageService);
|
||||
const containerService = new ContainerService(cryptoService);
|
||||
const authService = new AuthService(cryptoService, apiService, userService, tokenService, appIdService,
|
||||
i18nService, platformUtilsService, messagingService, false);
|
||||
i18nService, platformUtilsService, messagingService, null, logService, apiKeyService, false);
|
||||
const configurationService = new ConfigurationService(storageService, secureStorageService);
|
||||
const syncService = new SyncService(configurationService, logService, cryptoFunctionService, apiService,
|
||||
messagingService, i18nService);
|
||||
messagingService, i18nService, environmentService);
|
||||
const passwordGenerationService = new PasswordGenerationService(cryptoService, storageService, null);
|
||||
const policyService = new PolicyService(userService, storageService);
|
||||
|
||||
const analytics = new Analytics(window, () => true, platformUtilsService, storageService, appIdService);
|
||||
containerService.attachToWindow(window);
|
||||
environmentService.setUrlsFromStorage().then(() => {
|
||||
// Do nothing
|
||||
});
|
||||
|
||||
function refreshTokenCallback(): Promise<any> {
|
||||
return refreshToken(apiKeyService, authService);
|
||||
}
|
||||
|
||||
export function initFactory(): Function {
|
||||
return async () => {
|
||||
await environmentService.setUrlsFromStorage();
|
||||
await i18nService.init();
|
||||
await authService.init();
|
||||
authService.init();
|
||||
const htmlEl = window.document.documentElement;
|
||||
htmlEl.classList.add('os_' + platformUtilsService.getDeviceString());
|
||||
htmlEl.classList.add('locale_' + i18nService.translationLocale);
|
||||
@@ -93,7 +103,7 @@ export function initFactory(): Function {
|
||||
|
||||
let installAction = null;
|
||||
const installedVersion = await storageService.get<string>(ConstantsService.installedVersionKey);
|
||||
const currentVersion = platformUtilsService.getApplicationVersion();
|
||||
const currentVersion = await platformUtilsService.getApplicationVersion();
|
||||
if (installedVersion == null) {
|
||||
installAction = 'install';
|
||||
} else if (installedVersion !== currentVersion) {
|
||||
@@ -122,6 +132,7 @@ export function initFactory(): Function {
|
||||
ValidationService,
|
||||
AuthGuardService,
|
||||
LaunchGuardService,
|
||||
ModalService,
|
||||
{ provide: AuthServiceAbstraction, useValue: authService },
|
||||
{ provide: EnvironmentServiceAbstraction, useValue: environmentService },
|
||||
{ provide: TokenServiceAbstraction, useValue: tokenService },
|
||||
@@ -130,6 +141,7 @@ export function initFactory(): Function {
|
||||
{ provide: PlatformUtilsServiceAbstraction, useValue: platformUtilsService },
|
||||
{ provide: ApiServiceAbstraction, useValue: apiService },
|
||||
{ provide: UserServiceAbstraction, useValue: userService },
|
||||
{ provide: ApiKeyServiceAbstraction, useValue: apiKeyService },
|
||||
{ provide: MessagingServiceAbstraction, useValue: messagingService },
|
||||
{ provide: BroadcasterService, useValue: broadcasterService },
|
||||
{ provide: StorageServiceAbstraction, useValue: storageService },
|
||||
@@ -137,6 +149,9 @@ export function initFactory(): Function {
|
||||
{ provide: LogServiceAbstraction, useValue: logService },
|
||||
{ provide: ConfigurationService, useValue: configurationService },
|
||||
{ provide: SyncService, useValue: syncService },
|
||||
{ provide: PasswordGenerationServiceAbstraction, useValue: passwordGenerationService },
|
||||
{ provide: CryptoFunctionServiceAbstraction, useValue: cryptoFunctionService },
|
||||
{ provide: PolicyServiceAbstraction, useValue: policyService },
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
useFactory: initFactory,
|
||||
|
||||
@@ -14,35 +14,44 @@
|
||||
<strong *ngIf="syncRunning" class="text-success">{{'running' | i18n}}</strong>
|
||||
<strong *ngIf="!syncRunning" class="text-danger">{{'stopped' | i18n}}</strong>
|
||||
</p>
|
||||
<button #startBtn (click)="start()" [appApiAction]="startPromise" class="btn btn-primary" [disabled]="startBtn.loading">
|
||||
<i class="fa fa-play fa-fw" [hidden]="startBtn.loading"></i>
|
||||
<i class="fa fa-spinner fa-fw fa-spin" [hidden]="!startBtn.loading"></i>
|
||||
{{'startSync' | i18n}}
|
||||
</button>
|
||||
<form #startForm [appApiAction]="startPromise" class="d-inline">
|
||||
<button (click)="start()" class="btn btn-primary"
|
||||
[disabled]="startForm.loading">
|
||||
<i class="fa fa-play fa-fw" [hidden]="startForm.loading"></i>
|
||||
<i class="fa fa-spinner fa-fw fa-spin" [hidden]="!startForm.loading"></i>
|
||||
{{'startSync' | i18n}}
|
||||
</button>
|
||||
</form>
|
||||
<button (click)="stop()" class="btn btn-primary">
|
||||
<i class="fa fa-stop fa-fw"></i>
|
||||
{{'stopSync' | i18n}}
|
||||
</button>
|
||||
<button #syncBtn (click)="sync()" [appApiAction]="syncPromise" class="btn btn-primary" [disabled]="syncBtn.loading">
|
||||
<i class="fa fa-refresh fa-fw" [ngClass]="{'fa-spin': syncBtn.loading}"></i>
|
||||
{{'syncNow' | i18n}}
|
||||
</button>
|
||||
<form #syncForm [appApiAction]="syncPromise" class="d-inline">
|
||||
<button (click)="sync()" class="btn btn-primary"
|
||||
[disabled]="syncForm.loading">
|
||||
<i class="fa fa-refresh fa-fw" [ngClass]="{'fa-spin': syncForm.loading}"></i>
|
||||
{{'syncNow' | i18n}}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3 class="card-header">{{'testing' | i18n}}</h3>
|
||||
<div class="card-body">
|
||||
<p>{{'testingDesc' | i18n}}</p>
|
||||
<button #simBtn (click)="simulate()" [appApiAction]="simPromise" class="btn btn-primary" [disabled]="simBtn.loading">
|
||||
<i class="fa fa-spinner fa-fw fa-spin" [hidden]="!simBtn.loading"></i>
|
||||
<i class="fa fa-bug fa-fw" [hidden]="simBtn.loading"></i>
|
||||
{{'testNow' | i18n}}
|
||||
</button>
|
||||
<form #simForm [appApiAction]="simPromise" class="d-inline">
|
||||
<button (click)="simulate()" class="btn btn-primary"
|
||||
[disabled]="simForm.loading">
|
||||
<i class="fa fa-spinner fa-fw fa-spin" [hidden]="!simForm.loading"></i>
|
||||
<i class="fa fa-bug fa-fw" [hidden]="simForm.loading"></i>
|
||||
{{'testNow' | i18n}}
|
||||
</button>
|
||||
</form>
|
||||
<div class="form-check mt-2">
|
||||
<input class="form-check-input" type="checkbox" id="simSinceLast" [(ngModel)]="simSinceLast">
|
||||
<label class="form-check-label" for="simSinceLast">{{'testLastSync' | i18n}}</label>
|
||||
</div>
|
||||
<ng-container *ngIf="!simBtn.loading && (simUsers || simGroups)">
|
||||
<ng-container *ngIf="!simForm.loading && (simUsers || simGroups)">
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col-lg">
|
||||
|
||||
@@ -8,21 +8,20 @@ import {
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { MessagingService } from 'jslib/abstractions/messaging.service';
|
||||
import { StateService } from 'jslib/abstractions/state.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 { AzureDirectoryService } from '../../services/azure-directory.service';
|
||||
import { GSuiteDirectoryService } from '../../services/gsuite-directory.service';
|
||||
import { LdapDirectoryService } from '../../services/ldap-directory.service';
|
||||
import { SyncService } from '../../services/sync.service';
|
||||
|
||||
import { Entry } from '../../models/entry';
|
||||
import { GroupEntry } from '../../models/groupEntry';
|
||||
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 { BroadcasterService } from 'jslib-angular/services/broadcaster.service';
|
||||
|
||||
import { ConnectorUtils } from '../../utils';
|
||||
|
||||
const BroadcasterSubscriptionId = 'DashboardComponent';
|
||||
|
||||
@@ -36,7 +35,7 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
||||
simEnabledUsers: UserEntry[] = [];
|
||||
simDisabledUsers: UserEntry[] = [];
|
||||
simDeletedUsers: UserEntry[] = [];
|
||||
simPromise: Promise<any>;
|
||||
simPromise: Promise<SimResult>;
|
||||
simSinceLast: boolean = false;
|
||||
syncPromise: Promise<[GroupEntry[], UserEntry[]]>;
|
||||
startPromise: Promise<any>;
|
||||
@@ -103,63 +102,18 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
||||
this.simDisabledUsers = [];
|
||||
this.simDeletedUsers = [];
|
||||
|
||||
this.simPromise = new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const result = await this.syncService.sync(!this.simSinceLast, true);
|
||||
if (result[0] != null) {
|
||||
this.simGroups = result[0];
|
||||
}
|
||||
if (result[1] != null) {
|
||||
this.simUsers = result[1];
|
||||
}
|
||||
} catch (e) {
|
||||
this.simGroups = null;
|
||||
this.simUsers = null;
|
||||
reject(e || this.i18nService.t('syncError'));
|
||||
return;
|
||||
}
|
||||
|
||||
const userMap = new Map<string, UserEntry>();
|
||||
this.sort(this.simUsers);
|
||||
for (const u of this.simUsers) {
|
||||
userMap.set(u.externalId, u);
|
||||
if (u.deleted) {
|
||||
this.simDeletedUsers.push(u);
|
||||
} else if (u.disabled) {
|
||||
this.simDisabledUsers.push(u);
|
||||
} else {
|
||||
this.simEnabledUsers.push(u);
|
||||
}
|
||||
}
|
||||
|
||||
this.sort(this.simGroups);
|
||||
for (const g of this.simGroups) {
|
||||
if (g.userMemberExternalIds == null) {
|
||||
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.sort(anyG.users);
|
||||
}
|
||||
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
private sort(arr: Entry[]) {
|
||||
arr.sort((a, b) => {
|
||||
return this.i18nService.collator ? this.i18nService.collator.compare(a.displayName, b.displayName) :
|
||||
a.displayName.localeCompare(b.displayName);
|
||||
});
|
||||
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() {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<p>
|
||||
{{'bitwardenDirectoryConnector' | i18n}}
|
||||
<br /> {{'version' | i18n : version}}
|
||||
<br /> © 8bit Solutions LLC 2015-{{year}}
|
||||
<br /> © Bitwarden Inc. LLC 2015-{{year}}
|
||||
</p>
|
||||
<button class="btn btn-primary" type="button" (click)="update()" [disabled]="checkingForUpdate">
|
||||
<i class="fa fa-download fa-fw" [hidden]="checkingForUpdate"></i>
|
||||
|
||||
@@ -8,11 +8,11 @@ import {
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
|
||||
import { BroadcasterService } from 'jslib/angular/services/broadcaster.service';
|
||||
import { BroadcasterService } from 'jslib-angular/services/broadcaster.service';
|
||||
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { MessagingService } from 'jslib/abstractions/messaging.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.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 { ConfigurationService } from '../../services/configuration.service';
|
||||
|
||||
@@ -32,7 +32,7 @@ export class MoreComponent implements OnInit {
|
||||
private toasterService: ToasterService, private broadcasterService: BroadcasterService,
|
||||
private ngZone: NgZone, private changeDetectorRef: ChangeDetectorRef) { }
|
||||
|
||||
ngOnInit() {
|
||||
async ngOnInit() {
|
||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
|
||||
this.ngZone.run(async () => {
|
||||
switch (message.command) {
|
||||
@@ -51,7 +51,7 @@ export class MoreComponent implements OnInit {
|
||||
});
|
||||
|
||||
this.year = new Date().getFullYear().toString();
|
||||
this.version = this.platformUtilsService.getApplicationVersion();
|
||||
this.version = await this.platformUtilsService.getApplicationVersion();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
<div [hidden]="directory != directoryType.Ldap">
|
||||
<div class="form-group">
|
||||
<label for="hostname">{{'serverHostname' | i18n}}</label>
|
||||
<input type="text" class="form-control" id="hostname" name="Hostname" [(ngModel)]="ldap.hostname">
|
||||
<input type="text" class="form-control" id="hostname" name="Hostname"
|
||||
[(ngModel)]="ldap.hostname">
|
||||
<small class="text-muted form-text">{{'ex' | i18n}} ad.company.com</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@@ -22,37 +23,113 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="rootPath">{{'rootPath' | i18n}}</label>
|
||||
<input type="text" class="form-control" id="rootPath" name="RootPath" [(ngModel)]="ldap.rootPath">
|
||||
<input type="text" class="form-control" id="rootPath" name="RootPath"
|
||||
[(ngModel)]="ldap.rootPath">
|
||||
<small class="text-muted form-text">{{'ex' | i18n}} dc=company,dc=com</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="ssl" [(ngModel)]="ldap.ssl" name="SSL">
|
||||
<label class="form-check-label" for="ssl">{{'ldapSsl' | i18n}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="ad" [(ngModel)]="ldap.ad" name="AD">
|
||||
<label class="form-check-label" for="ad">{{'ldapAd' | i18n}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" *ngIf="!ldap.ad">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="pagedSearch"
|
||||
[(ngModel)]="ldap.pagedSearch" name="PagedSearch">
|
||||
<label class="form-check-label" for="pagedSearch">{{'ldapPagedResults' | i18n}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="ldapEncrypted" [(ngModel)]="ldap.ssl"
|
||||
name="Encrypted">
|
||||
<label class="form-check-label" for="ldapEncrypted">{{'ldapEncrypted' | i18n}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4" *ngIf="ldap.ssl">
|
||||
<div class="form-group">
|
||||
<div class="form-radio">
|
||||
<input class="form-radio-input" type="radio" [value]="false" id="ssl"
|
||||
[(ngModel)]="ldap.startTls" name="SSL">
|
||||
<label class="form-radio-label" for="ssl">{{'ldapSsl' | i18n}}</label>
|
||||
</div>
|
||||
<div class="form-radio">
|
||||
<input class="form-radio-input" type="radio" [value]="true" id="startTls"
|
||||
[(ngModel)]="ldap.startTls" name="StartTLS">
|
||||
<label class="form-radio-label" for="startTls">{{'ldapTls' | i18n}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4" *ngIf="ldap.startTls">
|
||||
<p>{{'ldapTlsUntrustedDesc' | i18n}}</p>
|
||||
<div class="form-group">
|
||||
<label for="tlsCaPath">{{'ldapTlsCa' | i18n}}</label>
|
||||
<input type="file" class="form-control-file mb-2" id="tlsCaPath_file"
|
||||
(change)="setSslPath('tlsCaPath')">
|
||||
<input type="text" class="form-control" id="tlsCaPath" name="TLSCaPath"
|
||||
[(ngModel)]="ldap.tlsCaPath">
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4" *ngIf="!ldap.startTls">
|
||||
<p>{{'ldapSslUntrustedDesc' | i18n}}</p>
|
||||
<div class="form-group">
|
||||
<label for="sslCertPath">{{'ldapSslCert' | i18n}}</label>
|
||||
<input type="file" class="form-control-file mb-2" id="sslCertPath_file"
|
||||
(change)="setSslPath('sslCertPath')">
|
||||
<input type="text" class="form-control" id="sslCertPath" name="SSLCertPath"
|
||||
[(ngModel)]="ldap.sslCertPath">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="sslKeyPath">{{'ldapSslKey' | i18n}}</label>
|
||||
<input type="file" class="form-control-file mb-2" id="sslKeyPath_file"
|
||||
(change)="setSslPath('sslKeyPath')">
|
||||
<input type="text" class="form-control" id="sslKeyPath" name="SSLKeyPath"
|
||||
[(ngModel)]="ldap.sslKeyPath">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="sslCaPath">{{'ldapSslCa' | i18n}}</label>
|
||||
<input type="file" class="form-control-file mb-2" id="sslCaPath_file"
|
||||
(change)="setSslPath('sslCaPath')">
|
||||
<input type="text" class="form-control" id="sslCaPath" name="SSLCaPath"
|
||||
[(ngModel)]="ldap.sslCaPath">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="certDoNotVerify"
|
||||
[(ngModel)]="ldap.sslAllowUnauthorized" name="CertDoNoVerify">
|
||||
<label class="form-check-label" for="certDoNotVerify">{{'ldapCertDoNotVerify' |
|
||||
i18n}}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" [hidden]="true">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="currentUser" [(ngModel)]="ldap.currentUser" name="CurrentUser">
|
||||
<input class="form-check-input" type="checkbox" id="currentUser"
|
||||
[(ngModel)]="ldap.currentUser" name="CurrentUser">
|
||||
<label class="form-check-label" for="currentUser">{{'currentUser' | i18n}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div [hidden]="ldap.currentUser">
|
||||
<div class="form-group">
|
||||
<label for="username">{{'username' | i18n}}</label>
|
||||
<input type="text" class="form-control" id="username" name="Username" [(ngModel)]="ldap.username">
|
||||
<input type="text" class="form-control" id="username" name="Username"
|
||||
[(ngModel)]="ldap.username">
|
||||
<small class="text-muted form-text" *ngIf="ldap.ad">{{'ex' | i18n}} company\admin</small>
|
||||
<small class="text-muted form-text" *ngIf="!ldap.ad">{{'ex' | i18n}} cn=admin,dc=company,dc=com</small>
|
||||
<small class="text-muted form-text" *ngIf="!ldap.ad">{{'ex' | i18n}}
|
||||
cn=admin,dc=company,dc=com</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">{{'password' | i18n}}</label>
|
||||
<input type="password" class="form-control" id="password" name="Password" [(ngModel)]="ldap.password">
|
||||
<div class="input-group">
|
||||
<input type="{{showLdapPassword ? 'text' : 'password'}}" class="form-control" id="password" name="Password"
|
||||
[(ngModel)]="ldap.password">
|
||||
<div class="input-group-append">
|
||||
<button type="button" class="btn btn-outline-secondary" appA11yTitle="{{'toggleVisibility' | i18n}}" (click)="toggleLdapPassword()">
|
||||
<i class="fa fa-lg" aria-hidden="true"[ngClass]="showLdapPassword ? 'fa-eye-slash' : 'fa-eye'"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -64,11 +141,20 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="applicationId">{{'applicationId' | i18n}}</label>
|
||||
<input type="text" class="form-control" id="applicationId" name="ApplicationId" [(ngModel)]="azure.applicationId">
|
||||
<input type="text" class="form-control" id="applicationId" name="ApplicationId"
|
||||
[(ngModel)]="azure.applicationId">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="secretKey">{{'secretKey' | i18n}}</label>
|
||||
<input type="text" class="form-control" id="secretKey" name="SecretKey" [(ngModel)]="azure.key">
|
||||
<div class="input-group">
|
||||
<input type="{{showAzureKey ? 'text' : 'password'}}" class="form-control" id="secretKey" name="SecretKey"
|
||||
[(ngModel)]="azure.key">
|
||||
<div class="input-group-append">
|
||||
<button type="button" class="btn btn-outline-secondary" appA11yTitle="{{'toggleVisibility' | i18n}}" (click)="toggleAzureKey()">
|
||||
<i class="fa fa-lg" aria-hidden="true"[ngClass]="showAzureKey ? 'fa-eye-slash' : 'fa-eye'"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div [hidden]="directory != directoryType.Okta">
|
||||
@@ -79,7 +165,42 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="oktaToken">{{'token' | i18n}}</label>
|
||||
<input type="text" class="form-control" id="oktaToken" name="OktaToken" [(ngModel)]="okta.token">
|
||||
<div class="input-group">
|
||||
<input type="{{showOktaKey ? 'text' : 'password'}}" class="form-control" id="oktaToken" name="OktaToken"
|
||||
[(ngModel)]="okta.token">
|
||||
<div class="input-group-append">
|
||||
<button type="button" class="btn btn-outline-secondary" appA11yTitle="{{'toggleVisibility' | i18n}}" (click)="toggleOktaKey()">
|
||||
<i class="fa fa-lg" aria-hidden="true"[ngClass]="showOktaKey ? 'fa-eye-slash' : 'fa-eye'"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div [hidden]="directory != directoryType.OneLogin">
|
||||
<div class="form-group">
|
||||
<label for="oneLoginClientId">{{'clientId' | i18n}}</label>
|
||||
<input type="text" class="form-control" id="oneLoginClientId" name="OneLoginClientId"
|
||||
[(ngModel)]="oneLogin.clientId">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="oneLoginClientSecret">{{'clientSecret' | i18n}}</label>
|
||||
<div class="input-group">
|
||||
<input type="{{showOneLoginSecret ? 'text' : 'password'}}" class="form-control" id="oneLoginClientSecret" name="OneLoginClientSecret"
|
||||
[(ngModel)]="oneLogin.clientSecret">
|
||||
<div class="input-group-append">
|
||||
<button type="button" class="btn btn-outline-secondary" appA11yTitle="{{'toggleVisibility' | i18n}}" (click)="toggleOneLoginSecret()">
|
||||
<i class="fa fa-lg" aria-hidden="true"[ngClass]="showOneLoginSecret ? 'fa-eye-slash' : 'fa-eye'"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="oneLoginRegion">{{'region' | i18n}}</label>
|
||||
<select class="form-control" id="oneLoginRegion" name="OneLoginRegion"
|
||||
[(ngModel)]="oneLogin.region">
|
||||
<option value="us">US</option>
|
||||
<option value="eu">EU</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div [hidden]="directory != directoryType.GSuite">
|
||||
@@ -90,43 +211,37 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="adminUser">{{'adminUser' | i18n}}</label>
|
||||
<input type="text" class="form-control" id="adminUser" name="AdminUser" [(ngModel)]="gsuite.adminUser">
|
||||
<input type="text" class="form-control" id="adminUser" name="AdminUser"
|
||||
[(ngModel)]="gsuite.adminUser">
|
||||
<small class="text-muted form-text">{{'ex' | i18n}} admin@company.com</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="customerId">{{'customerId' | i18n}}</label>
|
||||
<input type="text" class="form-control" id="customerId" name="CustomerId" [(ngModel)]="gsuite.customer">
|
||||
<input type="text" class="form-control" id="customerId" name="CustomerId"
|
||||
[(ngModel)]="gsuite.customer">
|
||||
<small class="text-muted form-text">{{'ex' | i18n}} 39204722352</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="keyFile">{{'jsonKeyFile' | i18n}}</label>
|
||||
<input type="file" class="form-control-file" id="keyFile" accept=".json" (change)="parseKeyFile()">
|
||||
<input type="file" class="form-control-file" id="keyFile" accept=".json"
|
||||
(change)="parseKeyFile()">
|
||||
<small class="text-muted form-text">{{'ex' | i18n}} My Project-jksd3jd223.json</small>
|
||||
</div>
|
||||
<div class="form-group" [hidden]="!gsuite.clientEmail">
|
||||
<label for="clientEmail">{{'clientEmail' | i18n}}</label>
|
||||
<input type="text" class="form-control" id="clientEmail" name="ClientEmail" [(ngModel)]="gsuite.clientEmail">
|
||||
<input type="text" class="form-control" id="clientEmail" name="ClientEmail"
|
||||
[(ngModel)]="gsuite.clientEmail">
|
||||
</div>
|
||||
<div class="form-group" [hidden]="!gsuite.privateKey">
|
||||
<label for="privateKey">{{'privateKey' | i18n}}</label>
|
||||
<textarea class="form-control text-monospace" id="privateKey" name="PrivateKey" [(ngModel)]="gsuite.privateKey">
|
||||
<textarea class="form-control text-monospace" id="privateKey" name="PrivateKey"
|
||||
[(ngModel)]="gsuite.privateKey">
|
||||
</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-3">
|
||||
<h3 class="card-header">{{'account' | i18n}}</h3>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label for="organizationId">{{'organization' | i18n}}</label>
|
||||
<select class="form-control" id="organizationId" name="OrganizationId" [(ngModel)]="organizationId">
|
||||
<option *ngFor="let o of organizationOptions" [ngValue]="o.value">{{o.name}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="card">
|
||||
@@ -134,49 +249,73 @@
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label for="interval">{{'interval' | i18n}}</label>
|
||||
<input type="number" min="5" class="form-control" id="interval" name="Interval" [(ngModel)]="sync.interval">
|
||||
<input type="number" min="5" class="form-control" id="interval" name="Interval"
|
||||
[(ngModel)]="sync.interval">
|
||||
<small class="text-muted form-text">{{'intervalMin' | i18n}}</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="removeDisabled" [(ngModel)]="sync.removeDisabled" name="RemoveDisabled">
|
||||
<input class="form-check-input" type="checkbox" id="removeDisabled"
|
||||
[(ngModel)]="sync.removeDisabled" name="RemoveDisabled">
|
||||
<label class="form-check-label" for="removeDisabled">{{'removeDisabled' | i18n}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="overwriteExisting"
|
||||
[(ngModel)]="sync.overwriteExisting" name="OverwriteExisting">
|
||||
<label class="form-check-label" for="overwriteExisting">{{'overwriteExisting' | i18n}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="largeImport" [(ngModel)]="sync.largeImport"
|
||||
name="LargeImport">
|
||||
<label class="form-check-label" for="largeImport">{{'largeImport' | i18n}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div [hidden]="directory != directoryType.Ldap">
|
||||
<div [hidden]="ldap.ad">
|
||||
<div class="form-group">
|
||||
<label for="memberAttribute">{{'memberAttribute' | i18n}}</label>
|
||||
<input type="text" class="form-control" id="memberAttribute" name="MemberAttribute" [(ngModel)]="sync.memberAttribute">
|
||||
<input type="text" class="form-control" id="memberAttribute" name="MemberAttribute"
|
||||
[(ngModel)]="sync.memberAttribute">
|
||||
<small class="text-muted form-text">{{'ex' | i18n}} uniqueMember</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="creationDateAttribute">{{'creationDateAttribute' | i18n}}</label>
|
||||
<input type="text" class="form-control" id="creationDateAttribute" name="CreationDateAttribute" [(ngModel)]="sync.creationDateAttribute">
|
||||
<input type="text" class="form-control" id="creationDateAttribute"
|
||||
name="CreationDateAttribute" [(ngModel)]="sync.creationDateAttribute">
|
||||
<small class="text-muted form-text">{{'ex' | i18n}} whenCreated</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="revisionDateAttribute">{{'revisionDateAttribute' | i18n}}</label>
|
||||
<input type="text" class="form-control" id="revisionDateAttribute" name="RevisionDateAttribute" [(ngModel)]="sync.revisionDateAttribute">
|
||||
<input type="text" class="form-control" id="revisionDateAttribute"
|
||||
name="RevisionDateAttribute" [(ngModel)]="sync.revisionDateAttribute">
|
||||
<small class="text-muted form-text">{{'ex' | i18n}} whenChanged</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div [hidden]="directory != directoryType.Ldap && directory != directoryType.OneLogin">
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="useEmailPrefixSuffix" [(ngModel)]="sync.useEmailPrefixSuffix" name="UseEmailPrefixSuffix">
|
||||
<label class="form-check-label" for="useEmailPrefixSuffix">{{'useEmailPrefixSuffix' | i18n}}</label>
|
||||
<input class="form-check-input" type="checkbox" id="useEmailPrefixSuffix"
|
||||
[(ngModel)]="sync.useEmailPrefixSuffix" name="UseEmailPrefixSuffix">
|
||||
<label class="form-check-label"
|
||||
for="useEmailPrefixSuffix">{{'useEmailPrefixSuffix' | i18n}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div [hidden]="!sync.useEmailPrefixSuffix">
|
||||
<div class="form-group" [hidden]="ldap.ad">
|
||||
<div class="form-group" [hidden]="ldap.ad || directory != directoryType.Ldap">
|
||||
<label for="emailPrefixAttribute">{{'emailPrefixAttribute' | i18n}}</label>
|
||||
<input type="text" class="form-control" id="emailPrefixAttribute" name="EmailPrefixAttribute" [(ngModel)]="sync.emailPrefixAttribute">
|
||||
<input type="text" class="form-control" id="emailPrefixAttribute"
|
||||
name="EmailPrefixAttribute" [(ngModel)]="sync.emailPrefixAttribute">
|
||||
<small class="text-muted form-text">{{'ex' | i18n}} accountName</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="emailSuffix">{{'emailSuffix' | i18n}}</label>
|
||||
<input type="text" class="form-control" id="emailSuffix" name="EmailSuffix" [(ngModel)]="sync.emailSuffix">
|
||||
<input type="text" class="form-control" id="emailSuffix" name="EmailSuffix"
|
||||
[(ngModel)]="sync.emailSuffix">
|
||||
<small class="text-muted form-text">{{'ex' | i18n}} @company.com</small>
|
||||
</div>
|
||||
</div>
|
||||
@@ -184,33 +323,43 @@
|
||||
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="syncUsers" [(ngModel)]="sync.users" name="SyncUsers">
|
||||
<input class="form-check-input" type="checkbox" id="syncUsers" [(ngModel)]="sync.users"
|
||||
name="SyncUsers">
|
||||
<label class="form-check-label" for="syncUsers">{{'syncUsers' | i18n}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div [hidden]="!sync.users">
|
||||
<div class="form-group">
|
||||
<label for="userFilter">{{'userFilter' | i18n}}</label>
|
||||
<textarea class="form-control" id="userFilter" name="UserFilter" [(ngModel)]="sync.userFilter"></textarea>
|
||||
<small class="text-muted form-text" *ngIf="directory === directoryType.Ldap">{{'ex' | i18n}} (&(givenName=John)(|(l=Dallas)(l=Austin)))</small>
|
||||
<small class="text-muted form-text" *ngIf="directory === directoryType.AzureActiveDirectory">{{'ex' | i18n}} exclude:joe@company.com</small>
|
||||
<small class="text-muted form-text" *ngIf="directory === directoryType.Okta">{{'ex' | i18n}} exclude:joe@company.com | profile.firstName eq "John"</small>
|
||||
<small class="text-muted form-text" *ngIf="directory === directoryType.GSuite">{{'ex' | i18n}} exclude:joe@company.com | orgName=Engineering</small>
|
||||
<textarea class="form-control" id="userFilter" name="UserFilter"
|
||||
[(ngModel)]="sync.userFilter"></textarea>
|
||||
<small class="text-muted form-text" *ngIf="directory === directoryType.Ldap">{{'ex' | i18n}}
|
||||
(&(givenName=John)(|(l=Dallas)(l=Austin)))</small>
|
||||
<small class="text-muted form-text"
|
||||
*ngIf="directory === directoryType.AzureActiveDirectory">{{'ex' | i18n}}
|
||||
exclude:joe@company.com</small>
|
||||
<small class="text-muted form-text" *ngIf="directory === directoryType.Okta">{{'ex' | i18n}}
|
||||
exclude:joe@company.com | profile.firstName eq "John"</small>
|
||||
<small class="text-muted form-text" *ngIf="directory === directoryType.GSuite">{{'ex' | i18n}}
|
||||
exclude:joe@company.com | orgName=Engineering</small>
|
||||
</div>
|
||||
<div class="form-group" [hidden]="directory != directoryType.Ldap">
|
||||
<label for="userPath">{{'userPath' | i18n}}</label>
|
||||
<input type="text" class="form-control" id="userPath" name="UserPath" [(ngModel)]="sync.userPath">
|
||||
<input type="text" class="form-control" id="userPath" name="UserPath"
|
||||
[(ngModel)]="sync.userPath">
|
||||
<small class="text-muted form-text">{{'ex' | i18n}} CN=Users</small>
|
||||
</div>
|
||||
<div [hidden]="directory != directoryType.Ldap || ldap.ad">
|
||||
<div class="form-group">
|
||||
<label for="userObjectClass">{{'userObjectClass' | i18n}}</label>
|
||||
<input type="text" class="form-control" id="userObjectClass" name="UserObjectClass" [(ngModel)]="sync.userObjectClass">
|
||||
<input type="text" class="form-control" id="userObjectClass" name="UserObjectClass"
|
||||
[(ngModel)]="sync.userObjectClass">
|
||||
<small class="text-muted form-text">{{'ex' | i18n}} inetOrgPerson</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="userEmailAttribute">{{'userEmailAttribute' | i18n}}</label>
|
||||
<input type="text" class="form-control" id="userEmailAttribute" name="UserEmailAttribute" [(ngModel)]="sync.userEmailAttribute">
|
||||
<input type="text" class="form-control" id="userEmailAttribute" name="UserEmailAttribute"
|
||||
[(ngModel)]="sync.userEmailAttribute">
|
||||
<small class="text-muted form-text">{{'ex' | i18n}} mail</small>
|
||||
</div>
|
||||
</div>
|
||||
@@ -218,34 +367,46 @@
|
||||
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="syncGroups" [(ngModel)]="sync.groups" name="SyncGroups">
|
||||
<label class="form-check-label" for="syncGroups">{{'syncGroups' | i18n}}</label>
|
||||
<input class="form-check-input" type="checkbox" id="syncGroups" [(ngModel)]="sync.groups"
|
||||
name="SyncGroups">
|
||||
<label class="form-check-label" for="syncGroups">{{(directory !== directoryType.OneLogin ?
|
||||
'syncGroups' : 'syncGroupsOneLogin') | i18n}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div [hidden]="!sync.groups">
|
||||
<div class="form-group">
|
||||
<label for="groupFilter">{{'groupFilter' | i18n}}</label>
|
||||
<textarea class="form-control" id="groupFilter" name="GroupFilter" [(ngModel)]="sync.groupFilter"></textarea>
|
||||
<small class="text-muted form-text" *ngIf="directory === directoryType.Ldap">{{'ex' | i18n}} (&!(name=Sales*)!(name=IT*))</small>
|
||||
<small class="text-muted form-text" *ngIf="directory === directoryType.AzureActiveDirectory">{{'ex' | i18n}} include:Sales,IT</small>
|
||||
<small class="text-muted form-text" *ngIf="directory === directoryType.Okta">{{'ex' | i18n}} include:Sales,IT | type eq "APP_GROUP"</small>
|
||||
<small class="text-muted form-text" *ngIf="directory === directoryType.GSuite">{{'ex' | i18n}} include:Sales,IT</small>
|
||||
<label for="groupFilter">{{(directory !== directoryType.OneLogin ? 'groupFilter' :
|
||||
'groupFilterOneLogin') | i18n}}</label>
|
||||
<textarea class="form-control" id="groupFilter" name="GroupFilter"
|
||||
[(ngModel)]="sync.groupFilter"></textarea>
|
||||
<small class="text-muted form-text" *ngIf="directory === directoryType.Ldap">{{'ex' | i18n}}
|
||||
(&!(name=Sales*)!(name=IT*))</small>
|
||||
<small class="text-muted form-text"
|
||||
*ngIf="directory === directoryType.AzureActiveDirectory">{{'ex' | i18n}}
|
||||
include:Sales,IT</small>
|
||||
<small class="text-muted form-text" *ngIf="directory === directoryType.Okta">{{'ex' | i18n}}
|
||||
include:Sales,IT | type eq "APP_GROUP"</small>
|
||||
<small class="text-muted form-text" *ngIf="directory === directoryType.GSuite">{{'ex' | i18n}}
|
||||
include:Sales,IT</small>
|
||||
</div>
|
||||
<div class="form-group" [hidden]="directory != directoryType.Ldap">
|
||||
<label for="groupPath">{{'groupPath' | i18n}}</label>
|
||||
<input type="text" class="form-control" id="groupPath" name="GroupPath" [(ngModel)]="sync.groupPath">
|
||||
<input type="text" class="form-control" id="groupPath" name="GroupPath"
|
||||
[(ngModel)]="sync.groupPath">
|
||||
<small class="text-muted form-text" *ngIf="!ldap.ad">{{'ex' | i18n}} CN=Groups</small>
|
||||
<small class="text-muted form-text" *ngIf="ldap.ad">{{'ex' | i18n}} CN=Users</small>
|
||||
</div>
|
||||
<div [hidden]="directory != directoryType.Ldap || ldap.ad">
|
||||
<div class="form-group">
|
||||
<label for="groupObjectClass">{{'groupObjectClass' | i18n}}</label>
|
||||
<input type="text" class="form-control" id="groupObjectClass" name="GroupObjectClass" [(ngModel)]="sync.groupObjectClass">
|
||||
<input type="text" class="form-control" id="groupObjectClass" name="GroupObjectClass"
|
||||
[(ngModel)]="sync.groupObjectClass">
|
||||
<small class="text-muted form-text">{{'ex' | i18n}} groupOfUniqueNames</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="groupNameAttribute">{{'groupNameAttribute' | i18n}}</label>
|
||||
<input type="text" class="form-control" id="groupNameAttribute" name="GroupNameAttribute" [(ngModel)]="sync.groupNameAttribute">
|
||||
<input type="text" class="form-control" id="groupNameAttribute" name="GroupNameAttribute"
|
||||
[(ngModel)]="sync.groupNameAttribute">
|
||||
<small class="text-muted form-text">{{'ex' | i18n}} name</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,10 +6,10 @@ import {
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { StateService } from 'jslib/abstractions/state.service';
|
||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
||||
import { StateService } from 'jslib-common/abstractions/state.service';
|
||||
|
||||
import { ProfileOrganizationResponse } from 'jslib/models/response/profileOrganizationResponse';
|
||||
import { ProfileOrganizationResponse } from 'jslib-common/models/response/profileOrganizationResponse';
|
||||
|
||||
import { ConfigurationService } from '../../services/configuration.service';
|
||||
|
||||
@@ -19,8 +19,11 @@ 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({
|
||||
selector: 'app-settings',
|
||||
templateUrl: 'settings.component.html',
|
||||
@@ -32,10 +35,13 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
gsuite = new GSuiteConfiguration();
|
||||
azure = new AzureConfiguration();
|
||||
okta = new OktaConfiguration();
|
||||
oneLogin = new OneLoginConfiguration();
|
||||
sync = new SyncConfiguration();
|
||||
organizationId: string;
|
||||
directoryOptions: any[];
|
||||
organizationOptions: any[];
|
||||
showLdapPassword: boolean = false;
|
||||
showAzureKey: boolean = false;
|
||||
showOktaKey: boolean = false;
|
||||
showOneLoginSecret: boolean = false;
|
||||
|
||||
constructor(private i18nService: I18nService, private configurationService: ConfigurationService,
|
||||
private changeDetectorRef: ChangeDetectorRef, private ngZone: NgZone,
|
||||
@@ -46,19 +52,11 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
{ 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.organizationOptions = [{ name: this.i18nService.t('select'), value: null }];
|
||||
const orgs = await this.stateService.get<ProfileOrganizationResponse[]>('profileOrganizations');
|
||||
if (orgs != null) {
|
||||
for (const org of orgs) {
|
||||
this.organizationOptions.push({ name: org.name, value: org.id });
|
||||
}
|
||||
}
|
||||
|
||||
this.organizationId = await this.configurationService.getOrganizationId();
|
||||
this.directory = await this.configurationService.getDirectoryType();
|
||||
this.ldap = (await this.configurationService.getDirectory<LdapConfiguration>(DirectoryType.Ldap)) ||
|
||||
this.ldap;
|
||||
@@ -68,6 +66,8 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -76,42 +76,20 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
async submit() {
|
||||
if (this.ldap.ad) {
|
||||
this.sync.creationDateAttribute = 'whenCreated';
|
||||
this.sync.revisionDateAttribute = 'whenChanged';
|
||||
this.sync.emailPrefixAttribute = 'sAMAccountName';
|
||||
this.sync.memberAttribute = 'member';
|
||||
this.sync.userObjectClass = 'person';
|
||||
this.sync.groupObjectClass = 'group';
|
||||
this.sync.userEmailAttribute = 'mail';
|
||||
this.sync.groupNameAttribute = 'name';
|
||||
|
||||
if (this.sync.groupPath == null) {
|
||||
this.sync.groupPath = 'CN=Users';
|
||||
}
|
||||
if (this.sync.userPath == null) {
|
||||
this.sync.userPath = 'CN=Users';
|
||||
}
|
||||
ConnectorUtils.adjustConfigForSave(this.ldap, this.sync);
|
||||
if (this.ldap != null && this.ldap.ad) {
|
||||
this.ldap.pagedSearch = true;
|
||||
}
|
||||
|
||||
if (this.sync.interval != null) {
|
||||
if (this.sync.interval <= 0) {
|
||||
this.sync.interval = null;
|
||||
} else if (this.sync.interval < 5) {
|
||||
this.sync.interval = 5;
|
||||
}
|
||||
}
|
||||
|
||||
await this.configurationService.saveOrganizationId(this.organizationId);
|
||||
await this.configurationService.saveDirectoryType(this.directory);
|
||||
await this.configurationService.saveDirectory(DirectoryType.Ldap, this.ldap);
|
||||
await this.configurationService.saveDirectory(DirectoryType.GSuite, this.gsuite);
|
||||
await this.configurationService.saveDirectory(DirectoryType.AzureActiveDirectory, this.azure);
|
||||
await this.configurationService.saveDirectory(DirectoryType.Okta, this.okta);
|
||||
await this.configurationService.saveDirectory(DirectoryType.OneLogin, this.oneLogin);
|
||||
await this.configurationService.saveSync(this.sync);
|
||||
}
|
||||
|
||||
async parseKeyFile() {
|
||||
parseKeyFile() {
|
||||
const filePicker = (document.getElementById('keyFile') as HTMLInputElement);
|
||||
if (filePicker.files == null || filePicker.files.length < 0) {
|
||||
return;
|
||||
@@ -119,10 +97,10 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.readAsText(filePicker.files[0], 'utf-8');
|
||||
reader.onload = (evt) => {
|
||||
reader.onload = evt => {
|
||||
this.ngZone.run(async () => {
|
||||
try {
|
||||
const result = JSON.parse((evt.target as FileReader).result);
|
||||
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;
|
||||
@@ -138,4 +116,38 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
filePicker.value = '';
|
||||
};
|
||||
}
|
||||
|
||||
setSslPath(id: string) {
|
||||
const filePicker = (document.getElementById(id + '_file') as HTMLInputElement);
|
||||
if (filePicker.files == null || filePicker.files.length < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
(this.ldap as any)[id] = filePicker.files[0].path;
|
||||
// reset file input
|
||||
// ref: https://stackoverflow.com/a/20552042
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
147
src/bwdc.ts
Normal file
147
src/bwdc.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import { LogLevelType } from 'jslib-common/enums/logLevelType';
|
||||
|
||||
import { AuthService } from './services/auth.service';
|
||||
|
||||
import { ConfigurationService } from './services/configuration.service';
|
||||
import { I18nService } from './services/i18n.service';
|
||||
import { KeytarSecureStorageService } from './services/keytarSecureStorage.service';
|
||||
import { LowdbStorageService } from './services/lowdbStorage.service';
|
||||
import { NodeApiService } from './services/nodeApi.service';
|
||||
import { SyncService } from './services/sync.service';
|
||||
|
||||
import { CliPlatformUtilsService } from 'jslib-node/cli/services/cliPlatformUtils.service';
|
||||
import { ConsoleLogService } from 'jslib-node/cli/services/consoleLog.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 { 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 { NoopMessagingService } from 'jslib-common/services/noopMessaging.service';
|
||||
import { PasswordGenerationService } from 'jslib-common/services/passwordGeneration.service';
|
||||
import { TokenService } from 'jslib-common/services/token.service';
|
||||
import { UserService } from 'jslib-common/services/user.service';
|
||||
|
||||
import { StorageService as StorageServiceAbstraction } from 'jslib-common/abstractions/storage.service';
|
||||
|
||||
import { Program } from './program';
|
||||
import { refreshToken } from './services/api.service';
|
||||
|
||||
// tslint:disable-next-line
|
||||
const packageJson = require('./package.json');
|
||||
|
||||
export class Main {
|
||||
dataFilePath: string;
|
||||
logService: ConsoleLogService;
|
||||
messagingService: NoopMessagingService;
|
||||
storageService: LowdbStorageService;
|
||||
secureStorageService: StorageServiceAbstraction;
|
||||
i18nService: I18nService;
|
||||
platformUtilsService: CliPlatformUtilsService;
|
||||
constantsService: ConstantsService;
|
||||
cryptoService: CryptoService;
|
||||
tokenService: TokenService;
|
||||
appIdService: AppIdService;
|
||||
apiService: NodeApiService;
|
||||
environmentService: EnvironmentService;
|
||||
apiKeyService: ApiKeyService;
|
||||
userService: UserService;
|
||||
containerService: ContainerService;
|
||||
cryptoFunctionService: NodeCryptoFunctionService;
|
||||
authService: AuthService;
|
||||
configurationService: ConfigurationService;
|
||||
syncService: SyncService;
|
||||
passwordGenerationService: PasswordGenerationService;
|
||||
program: Program;
|
||||
|
||||
constructor() {
|
||||
const applicationName = 'Bitwarden Directory Connector';
|
||||
if (process.env.BITWARDENCLI_CONNECTOR_APPDATA_DIR) {
|
||||
this.dataFilePath = path.resolve(process.env.BITWARDENCLI_CONNECTOR_APPDATA_DIR);
|
||||
} else if (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'))) {
|
||||
this.dataFilePath = path.join(__dirname, 'bitwarden-connector-appdata');
|
||||
} else if (process.platform === 'darwin') {
|
||||
this.dataFilePath = path.join(process.env.HOME, 'Library/Application Support/' + applicationName);
|
||||
} else if (process.platform === 'win32') {
|
||||
this.dataFilePath = path.join(process.env.APPDATA, applicationName);
|
||||
} else if (process.env.XDG_CONFIG_HOME) {
|
||||
this.dataFilePath = path.join(process.env.XDG_CONFIG_HOME, applicationName);
|
||||
} else {
|
||||
this.dataFilePath = path.join(process.env.HOME, '.config/' + applicationName);
|
||||
}
|
||||
|
||||
const plaintextSecrets = process.env.BITWARDENCLI_CONNECTOR_PLAINTEXT_SECRETS === 'true';
|
||||
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() {
|
||||
await this.init();
|
||||
this.program.run();
|
||||
}
|
||||
|
||||
async logout() {
|
||||
await this.tokenService.clearToken();
|
||||
await this.apiKeyService.clear();
|
||||
}
|
||||
|
||||
private async init() {
|
||||
await this.storageService.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.storageService.get<string>(ConstantsService.localeKey);
|
||||
await this.i18nService.init(locale);
|
||||
this.authService.init();
|
||||
|
||||
const installedVersion = await this.storageService.get<string>(ConstantsService.installedVersionKey);
|
||||
const currentVersion = await this.platformUtilsService.getApplicationVersion();
|
||||
if (installedVersion == null || installedVersion !== currentVersion) {
|
||||
await this.storageService.save(ConstantsService.installedVersionKey, currentVersion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const main = new Main();
|
||||
main.run();
|
||||
22
src/commands/clearCache.command.ts
Normal file
22
src/commands/clearCache.command.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import * as program from 'commander';
|
||||
|
||||
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';
|
||||
|
||||
export class ClearCacheCommand {
|
||||
constructor(private configurationService: ConfigurationService, private i18nService: I18nService) { }
|
||||
|
||||
async run(cmd: program.OptionValues): Promise<Response> {
|
||||
try {
|
||||
await this.configurationService.clearStatefulSettings(true);
|
||||
const res = new MessageResponse(this.i18nService.t('syncCacheCleared'), null);
|
||||
return Response.success(res);
|
||||
} catch (e) {
|
||||
return Response.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
150
src/commands/config.command.ts
Normal file
150
src/commands/config.command.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import * as program from 'commander';
|
||||
|
||||
import { EnvironmentService } from 'jslib-common/abstractions/environment.service';
|
||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
||||
|
||||
import { ConfigurationService } from '../services/configuration.service';
|
||||
|
||||
import { DirectoryType } from '../enums/directoryType';
|
||||
|
||||
import { Response } from 'jslib-node/cli/models/response';
|
||||
import { MessageResponse } from 'jslib-node/cli/models/response/messageResponse';
|
||||
|
||||
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';
|
||||
|
||||
import { NodeUtils } from 'jslib-common/misc/nodeUtils';
|
||||
|
||||
export class ConfigCommand {
|
||||
private directory: DirectoryType;
|
||||
private ldap = new LdapConfiguration();
|
||||
private gsuite = new GSuiteConfiguration();
|
||||
private azure = new AzureConfiguration();
|
||||
private okta = new OktaConfiguration();
|
||||
private oneLogin = new OneLoginConfiguration();
|
||||
private sync = new SyncConfiguration();
|
||||
|
||||
constructor(private environmentService: EnvironmentService, private i18nService: I18nService,
|
||||
private configurationService: ConfigurationService) { }
|
||||
|
||||
async run(setting: string, value: string, options: program.OptionValues): Promise<Response> {
|
||||
setting = setting.toLowerCase();
|
||||
if (value == null || value === '') {
|
||||
if (options.secretfile) {
|
||||
value = await NodeUtils.readFirstLine(options.secretfile);
|
||||
} else if (options.secretenv && 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);
|
||||
}
|
||||
|
||||
private async setServer(url: string) {
|
||||
url = (url === 'null' || url === 'bitwarden.com' || url === 'https://bitwarden.com' ? null : url);
|
||||
await this.environmentService.setUrls({
|
||||
base: url,
|
||||
});
|
||||
}
|
||||
|
||||
private async setDirectory(type: string) {
|
||||
const dir = parseInt(type, null);
|
||||
if (dir < DirectoryType.Ldap || dir > DirectoryType.OneLogin) {
|
||||
throw new Error('Invalid directory type value.');
|
||||
}
|
||||
await this.loadConfig();
|
||||
this.directory = dir;
|
||||
await this.saveConfig();
|
||||
}
|
||||
|
||||
private async setLdapPassword(password: string) {
|
||||
await this.loadConfig();
|
||||
this.ldap.password = password;
|
||||
await this.saveConfig();
|
||||
}
|
||||
|
||||
private async setGSuiteKey(key: string) {
|
||||
await this.loadConfig();
|
||||
this.gsuite.privateKey = key != null ? key.trimLeft() : null;
|
||||
await this.saveConfig();
|
||||
}
|
||||
|
||||
private async setAzureKey(key: string) {
|
||||
await this.loadConfig();
|
||||
this.azure.key = key;
|
||||
await this.saveConfig();
|
||||
}
|
||||
|
||||
private async setOktaToken(token: string) {
|
||||
await this.loadConfig();
|
||||
this.okta.token = token;
|
||||
await this.saveConfig();
|
||||
}
|
||||
|
||||
private async setOneLoginSecret(secret: string) {
|
||||
await this.loadConfig();
|
||||
this.oneLogin.clientSecret = secret;
|
||||
await this.saveConfig();
|
||||
}
|
||||
|
||||
private async loadConfig() {
|
||||
this.directory = await this.configurationService.getDirectoryType();
|
||||
this.ldap = (await this.configurationService.getDirectory<LdapConfiguration>(DirectoryType.Ldap)) ||
|
||||
this.ldap;
|
||||
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() {
|
||||
ConnectorUtils.adjustConfigForSave(this.ldap, this.sync);
|
||||
await this.configurationService.saveDirectoryType(this.directory);
|
||||
await this.configurationService.saveDirectory(DirectoryType.Ldap, this.ldap);
|
||||
await this.configurationService.saveDirectory(DirectoryType.GSuite, this.gsuite);
|
||||
await this.configurationService.saveDirectory(DirectoryType.AzureActiveDirectory, this.azure);
|
||||
await this.configurationService.saveDirectory(DirectoryType.Okta, this.okta);
|
||||
await this.configurationService.saveDirectory(DirectoryType.OneLogin, this.oneLogin);
|
||||
await this.configurationService.saveSync(this.sync);
|
||||
}
|
||||
}
|
||||
29
src/commands/lastSync.command.ts
Normal file
29
src/commands/lastSync.command.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import * as program from 'commander';
|
||||
|
||||
import { ConfigurationService } from '../services/configuration.service';
|
||||
|
||||
import { Response } from 'jslib-node/cli/models/response';
|
||||
import { StringResponse } from 'jslib-node/cli/models/response/stringResponse';
|
||||
|
||||
export class LastSyncCommand {
|
||||
constructor(private configurationService: ConfigurationService) { }
|
||||
|
||||
async run(object: string): Promise<Response> {
|
||||
try {
|
||||
switch (object.toLowerCase()) {
|
||||
case 'groups':
|
||||
const groupsDate = await this.configurationService.getLastGroupSyncDate();
|
||||
const groupsRes = new StringResponse(groupsDate == null ? null : groupsDate.toISOString());
|
||||
return Response.success(groupsRes);
|
||||
case 'users':
|
||||
const usersDate = await this.configurationService.getLastUserSyncDate();
|
||||
const usersRes = new StringResponse(usersDate == null ? null : usersDate.toISOString());
|
||||
return Response.success(usersRes);
|
||||
default:
|
||||
return Response.badRequest('Unknown object.');
|
||||
}
|
||||
} catch (e) {
|
||||
return Response.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
23
src/commands/sync.command.ts
Normal file
23
src/commands/sync.command.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
||||
|
||||
import { SyncService } from '../services/sync.service';
|
||||
|
||||
import { Response } from 'jslib-node/cli/models/response';
|
||||
import { MessageResponse } from 'jslib-node/cli/models/response/messageResponse';
|
||||
|
||||
export class SyncCommand {
|
||||
constructor(private syncService: SyncService, private i18nService: I18nService) { }
|
||||
|
||||
async run(): Promise<Response> {
|
||||
try {
|
||||
const result = await this.syncService.sync(false, false);
|
||||
const groupCount = result[0] != null ? result[0].length : 0;
|
||||
const userCount = result[1] != null ? result[1].length : 0;
|
||||
const res = new MessageResponse(this.i18nService.t('syncingComplete'),
|
||||
this.i18nService.t('syncCounts', groupCount.toString(), userCount.toString()));
|
||||
return Response.success(res);
|
||||
} catch (e) {
|
||||
return Response.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
24
src/commands/test.command.ts
Normal file
24
src/commands/test.command.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import * as program from 'commander';
|
||||
|
||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
||||
|
||||
import { SyncService } from '../services/sync.service';
|
||||
|
||||
import { ConnectorUtils } from '../utils';
|
||||
|
||||
import { Response } from 'jslib-node/cli/models/response';
|
||||
import { TestResponse } from '../models/response/testResponse';
|
||||
|
||||
export class TestCommand {
|
||||
constructor(private syncService: SyncService, private i18nService: I18nService) { }
|
||||
|
||||
async run(cmd: program.OptionValues): Promise<Response> {
|
||||
try {
|
||||
const result = await ConnectorUtils.simulate(this.syncService, this.i18nService, cmd.last || false);
|
||||
const res = new TestResponse(result);
|
||||
return Response.success(res);
|
||||
} catch (e) {
|
||||
return Response.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,4 +3,5 @@ export enum DirectoryType {
|
||||
AzureActiveDirectory = 1,
|
||||
GSuite = 2,
|
||||
Okta = 3,
|
||||
OneLogin = 4,
|
||||
}
|
||||
|
||||
1
src/global.d.ts
vendored
1
src/global.d.ts
vendored
@@ -1,2 +1,3 @@
|
||||
declare function escape(s: string): string;
|
||||
declare function unescape(s: string): string;
|
||||
declare module 'duo_web_sdk';
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="Content-Security-Policy" 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>
|
||||
<base href="">
|
||||
|
||||
@@ -20,12 +20,30 @@
|
||||
"emailRequired": {
|
||||
"message": "Email address is required."
|
||||
},
|
||||
"clientIdRequired": {
|
||||
"message": "Client Id is required."
|
||||
},
|
||||
"invalidClientId": {
|
||||
"message": "Invalid Client Id provided."
|
||||
},
|
||||
"clientSecretRequired": {
|
||||
"message": "Client Secret is required."
|
||||
},
|
||||
"orgApiKeyRequired": {
|
||||
"message": "Api Key must belong to an Organization"
|
||||
},
|
||||
"failedToSaveCredentials": {
|
||||
"message": "Failed to save credentials"
|
||||
},
|
||||
"invalidEmail": {
|
||||
"message": "Invalid email address."
|
||||
},
|
||||
"masterPassRequired": {
|
||||
"message": "Master password is required."
|
||||
},
|
||||
"missingRequiredInput": {
|
||||
"message": "Missing required input."
|
||||
},
|
||||
"unexpectedError": {
|
||||
"message": "An unexpected error has occurred."
|
||||
},
|
||||
@@ -129,7 +147,7 @@
|
||||
"message": "Self-hosted Environment"
|
||||
},
|
||||
"selfHostedEnvironmentFooter": {
|
||||
"message": "Specify the base URL of your on-premise hosted Bitwarden installation."
|
||||
"message": "Specify the base URL of your on-premises hosted Bitwarden installation."
|
||||
},
|
||||
"customEnvironment": {
|
||||
"message": "Custom Environment"
|
||||
@@ -140,6 +158,9 @@
|
||||
"baseUrl": {
|
||||
"message": "Server URL"
|
||||
},
|
||||
"webVaultUrl": {
|
||||
"message": "Web Vault Server URL"
|
||||
},
|
||||
"apiUrl": {
|
||||
"message": "API Server URL"
|
||||
},
|
||||
@@ -399,6 +420,12 @@
|
||||
"groupFilter": {
|
||||
"message": "Group Filter"
|
||||
},
|
||||
"syncGroupsOneLogin": {
|
||||
"message": "Sync roles"
|
||||
},
|
||||
"groupFilterOneLogin": {
|
||||
"message": "Role Filter"
|
||||
},
|
||||
"groupObjectClass": {
|
||||
"message": "Group Object Class"
|
||||
},
|
||||
@@ -414,12 +441,55 @@
|
||||
"sync": {
|
||||
"message": "Sync"
|
||||
},
|
||||
"duplicateEmails": {
|
||||
"message": "Emails must be unique. Multiple entries pulled with the following emails:",
|
||||
"desription": "Error message displayed when duplicate email addresses are synced. Followed by a list of duplicate emails."
|
||||
},
|
||||
"andMore": {
|
||||
"message": "and $NUMBER$ more...",
|
||||
"placeholders": {
|
||||
"NUMBER": {
|
||||
"content": "$1",
|
||||
"example": "10"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ldapEncrypted": {
|
||||
"message": "This server uses an encrypted connection"
|
||||
},
|
||||
"ldapTls": {
|
||||
"message": "Use TLS (STARTTLS)"
|
||||
},
|
||||
"ldapTlsCa": {
|
||||
"message": "Certificate CA Chain (PEM)"
|
||||
},
|
||||
"ldapSsl": {
|
||||
"message": "This server uses SSL (LDAPS)"
|
||||
"message": "Use SSL (LDAPS)"
|
||||
},
|
||||
"ldapTlsUntrustedDesc": {
|
||||
"message": "If your LDAP server uses a self-signed certificate for STARTTLS, you can configure certificate options below."
|
||||
},
|
||||
"ldapSslUntrustedDesc": {
|
||||
"message": "If your LDAPS server uses an untrusted certificate you can configure certificate options below."
|
||||
},
|
||||
"ldapSslCa": {
|
||||
"message": "Certificate CA Chain (PEM)"
|
||||
},
|
||||
"ldapSslCert": {
|
||||
"message": "Certificate (PEM)"
|
||||
},
|
||||
"ldapSslKey": {
|
||||
"message": "Certificate Private Key (PEM)"
|
||||
},
|
||||
"ldapCertDoNotVerify": {
|
||||
"message": "Do not verify server certificates (not recommended)."
|
||||
},
|
||||
"ldapAd": {
|
||||
"message": "This server uses Active Directory"
|
||||
},
|
||||
"ldapPagedResults": {
|
||||
"message": "This server pages search results"
|
||||
},
|
||||
"select": {
|
||||
"message": "Select"
|
||||
},
|
||||
@@ -501,6 +571,9 @@
|
||||
"noGroups": {
|
||||
"message": "No groups to list."
|
||||
},
|
||||
"syncingComplete": {
|
||||
"message": "Syncing complete."
|
||||
},
|
||||
"syncingStarted": {
|
||||
"message": "Syncing started."
|
||||
},
|
||||
@@ -533,7 +606,7 @@
|
||||
"message": "Welcome to the Bitwarden Directory Connector"
|
||||
},
|
||||
"logInDesc": {
|
||||
"message": "Log in as an organization admin user below."
|
||||
"message": "Log in with an organization API key below."
|
||||
},
|
||||
"dirConfigIncomplete": {
|
||||
"message": "Directory configuration incomplete."
|
||||
@@ -550,5 +623,144 @@
|
||||
},
|
||||
"hideToTray": {
|
||||
"message": "Hide to Tray"
|
||||
},
|
||||
"alwaysOnTop": {
|
||||
"message": "Always on Top",
|
||||
"description": "Application window should always stay on top of other windows"
|
||||
},
|
||||
"hideToMenuBar": {
|
||||
"message": "Hide to Menu Bar"
|
||||
},
|
||||
"savedSetting": {
|
||||
"message": "Saved setting `$SETTING_NAME$`.",
|
||||
"placeholders": {
|
||||
"setting_name": {
|
||||
"content": "$1",
|
||||
"example": "server"
|
||||
}
|
||||
}
|
||||
},
|
||||
"largeImport": {
|
||||
"message": "More than 2000 users or groups are expected to sync."
|
||||
},
|
||||
"overwriteExisting": {
|
||||
"message": "Overwrite existing organization users based on current sync settings."
|
||||
},
|
||||
"clientId": {
|
||||
"message": "Client ID"
|
||||
},
|
||||
"clientSecret": {
|
||||
"message": "Client Secret"
|
||||
},
|
||||
"region": {
|
||||
"message": "Region"
|
||||
},
|
||||
"enterpriseSingleSignOn": {
|
||||
"message": "Enterprise Single Sign-On"
|
||||
},
|
||||
"setMasterPassword": {
|
||||
"message": "Set Master Password"
|
||||
},
|
||||
"ssoCompleteRegistration": {
|
||||
"message": "In order to complete logging in with SSO, please set a master password to access and protect your vault."
|
||||
},
|
||||
"newMasterPass": {
|
||||
"message": "New Master Password"
|
||||
},
|
||||
"confirmNewMasterPass": {
|
||||
"message": "Confirm New Master Password"
|
||||
},
|
||||
"masterPasswordPolicyInEffect": {
|
||||
"message": "One or more organization policies require your master password to meet the following requirements:"
|
||||
},
|
||||
"policyInEffectMinComplexity": {
|
||||
"message": "Minimum complexity score of $SCORE$",
|
||||
"placeholders": {
|
||||
"score": {
|
||||
"content": "$1",
|
||||
"example": "4"
|
||||
}
|
||||
}
|
||||
},
|
||||
"policyInEffectMinLength": {
|
||||
"message": "Minimum length of $LENGTH$",
|
||||
"placeholders": {
|
||||
"length": {
|
||||
"content": "$1",
|
||||
"example": "14"
|
||||
}
|
||||
}
|
||||
},
|
||||
"policyInEffectUppercase": {
|
||||
"message": "Contain one or more uppercase characters"
|
||||
},
|
||||
"policyInEffectLowercase": {
|
||||
"message": "Contain one or more lowercase characters"
|
||||
},
|
||||
"policyInEffectNumbers": {
|
||||
"message": "Contain one or more numbers"
|
||||
},
|
||||
"policyInEffectSpecial": {
|
||||
"message": "Contain one or more of the following special characters $CHARS$",
|
||||
"placeholders": {
|
||||
"chars": {
|
||||
"content": "$1",
|
||||
"example": "!@#$%^&*"
|
||||
}
|
||||
}
|
||||
},
|
||||
"masterPassDesc": {
|
||||
"message": "The master password is the password you use to access your vault. It is very important that you do not forget your master password. There is no way to recover the password in the event that you forget it."
|
||||
},
|
||||
"reTypeMasterPass": {
|
||||
"message": "Re-type Master Password"
|
||||
},
|
||||
"masterPassHint": {
|
||||
"message": "Master Password Hint (optional)"
|
||||
},
|
||||
"masterPassHintDesc": {
|
||||
"message": "A master password hint can help you remember your password if you forget it."
|
||||
},
|
||||
"strong": {
|
||||
"message": "Strong",
|
||||
"description": "ex. A strong password. Scale: Weak -> Good -> Strong"
|
||||
},
|
||||
"good": {
|
||||
"message": "Good",
|
||||
"description": "ex. A good password. Scale: Weak -> Good -> Strong"
|
||||
},
|
||||
"weak": {
|
||||
"message": "Weak",
|
||||
"description": "ex. A weak password. Scale: Weak -> Good -> Strong"
|
||||
},
|
||||
"weakMasterPassword": {
|
||||
"message": "Weak Master Password"
|
||||
},
|
||||
"weakMasterPasswordDesc": {
|
||||
"message": "The master password you have chosen is weak. You should use a strong master password (or a passphrase) to properly protect your Bitwarden account. Are you sure you want to use this master password?"
|
||||
},
|
||||
"errorOccurred": {
|
||||
"message": "An error has occurred."
|
||||
},
|
||||
"error": {
|
||||
"message": "Error"
|
||||
},
|
||||
"masterPassLength": {
|
||||
"message": "Master password must be at least 8 characters long."
|
||||
},
|
||||
"masterPassDoesntMatch": {
|
||||
"message": "Master password confirmation does not match."
|
||||
},
|
||||
"masterPasswordPolicyRequirementsNotMet": {
|
||||
"message": "Your new master password does not meet the policy requirements."
|
||||
},
|
||||
"loading": {
|
||||
"message": "Loading"
|
||||
},
|
||||
"setMasterPasswordRedirect": {
|
||||
"message": "In order to log in with SSO from the Directory Connector, you must first log in through the web vault to set your master password."
|
||||
},
|
||||
"launchWebVault": {
|
||||
"message": "Launch Web Vault"
|
||||
}
|
||||
}
|
||||
|
||||
53
src/main.ts
53
src/main.ts
@@ -1,23 +1,22 @@
|
||||
import { app, BrowserWindow } from 'electron';
|
||||
import { app } from 'electron';
|
||||
import * as path from 'path';
|
||||
|
||||
import { MenuMain } from './main/menu.main';
|
||||
import { MessagingMain } from './main/messaging.main';
|
||||
import { I18nService } from './services/i18n.service';
|
||||
|
||||
import { LowdbStorageService } from 'jslib/services/lowdbStorage.service';
|
||||
|
||||
import { KeytarStorageListener } from 'jslib/electron/keytarStorageListener';
|
||||
import { ElectronLogService } from 'jslib/electron/services/electronLog.service';
|
||||
import { ElectronMainMessagingService } from 'jslib/electron/services/electronMainMessaging.service';
|
||||
import { TrayMain } from 'jslib/electron/tray.main';
|
||||
import { UpdaterMain } from 'jslib/electron/updater.main';
|
||||
import { WindowMain } from 'jslib/electron/window.main';
|
||||
import { KeytarStorageListener } from 'jslib-electron/keytarStorageListener';
|
||||
import { ElectronLogService } from 'jslib-electron/services/electronLog.service';
|
||||
import { ElectronMainMessagingService } from 'jslib-electron/services/electronMainMessaging.service';
|
||||
import { ElectronStorageService } from 'jslib-electron/services/electronStorage.service';
|
||||
import { TrayMain } from 'jslib-electron/tray.main';
|
||||
import { UpdaterMain } from 'jslib-electron/updater.main';
|
||||
import { WindowMain } from 'jslib-electron/window.main';
|
||||
|
||||
export class Main {
|
||||
logService: ElectronLogService;
|
||||
i18nService: I18nService;
|
||||
storageService: LowdbStorageService;
|
||||
storageService: ElectronStorageService;
|
||||
messagingService: ElectronMainMessagingService;
|
||||
keytarStorageListener: KeytarStorageListener;
|
||||
|
||||
@@ -42,7 +41,7 @@ export class Main {
|
||||
app.setPath('logs', path.join(app.getPath('userData'), 'logs'));
|
||||
|
||||
const args = process.argv.slice(1);
|
||||
const watch = args.some((val) => val === '--watch');
|
||||
const watch = args.some(val => val === '--watch');
|
||||
|
||||
if (watch) {
|
||||
// tslint:disable-next-line
|
||||
@@ -51,9 +50,9 @@ export class Main {
|
||||
|
||||
this.logService = new ElectronLogService(null, app.getPath('userData'));
|
||||
this.i18nService = new I18nService('en', './locales/');
|
||||
this.storageService = new LowdbStorageService(null, app.getPath('userData'));
|
||||
this.storageService = new ElectronStorageService(app.getPath('userData'));
|
||||
|
||||
this.windowMain = new WindowMain(this.storageService, 800, 600);
|
||||
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');
|
||||
@@ -61,18 +60,17 @@ export class Main {
|
||||
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.messagingService = new ElectronMainMessagingService(this.windowMain, message => {
|
||||
this.messagingMain.onMessage(message);
|
||||
});
|
||||
|
||||
this.keytarStorageListener = new KeytarStorageListener('Bitwarden Directory Connector');
|
||||
this.keytarStorageListener = new KeytarStorageListener('Bitwarden Directory Connector', null);
|
||||
}
|
||||
|
||||
bootstrap() {
|
||||
this.storageService.init();
|
||||
this.keytarStorageListener.init();
|
||||
this.windowMain.init().then(async () => {
|
||||
await this.i18nService.init(app.getLocale());
|
||||
@@ -80,11 +78,32 @@ export class Main {
|
||||
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();
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
|
||||
import { Main } from '../main';
|
||||
|
||||
import { BaseMenu } from 'jslib/electron/baseMenu';
|
||||
import { BaseMenu } from 'jslib-electron/baseMenu';
|
||||
|
||||
export class MenuMain extends BaseMenu {
|
||||
menu: Menu;
|
||||
@@ -48,11 +48,19 @@ export class MenuMain extends BaseMenu {
|
||||
template[template.length - 1].submenu = this.macWindowSubmenuOptions;
|
||||
}
|
||||
|
||||
(template[template.length - 1].submenu as MenuItemConstructorOptions[]).splice(1, 0, {
|
||||
label: this.main.i18nService.t('hideToTray'),
|
||||
click: () => this.main.messagingService.send('hideToTray'),
|
||||
accelerator: 'CmdOrCtrl+Shift+M',
|
||||
});
|
||||
(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);
|
||||
|
||||
@@ -3,9 +3,9 @@ import {
|
||||
ipcMain,
|
||||
} from 'electron';
|
||||
|
||||
import { TrayMain } from 'jslib/electron/tray.main';
|
||||
import { UpdaterMain } from 'jslib/electron/updater.main';
|
||||
import { WindowMain } from 'jslib/electron/window.main';
|
||||
import { TrayMain } from 'jslib-electron/tray.main';
|
||||
import { UpdaterMain } from 'jslib-electron/updater.main';
|
||||
import { WindowMain } from 'jslib-electron/window.main';
|
||||
|
||||
import { MenuMain } from './menu.main';
|
||||
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { DirectoryType } from '../enums/directoryType';
|
||||
|
||||
export class LdapConfiguration {
|
||||
ssl = false;
|
||||
startTls = false;
|
||||
tlsCaPath: string;
|
||||
sslAllowUnauthorized = false;
|
||||
sslCertPath: string;
|
||||
sslKeyPath: string;
|
||||
sslCaPath: string;
|
||||
hostname: string;
|
||||
port = 389;
|
||||
domain: string;
|
||||
@@ -10,4 +14,5 @@ export class LdapConfiguration {
|
||||
username: string;
|
||||
password: string;
|
||||
ad = true;
|
||||
pagedSearch = true;
|
||||
}
|
||||
|
||||
5
src/models/oneLoginConfiguration.ts
Normal file
5
src/models/oneLoginConfiguration.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export class OneLoginConfiguration {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
region = 'us';
|
||||
}
|
||||
13
src/models/response/groupResponse.ts
Normal file
13
src/models/response/groupResponse.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { GroupEntry } from '../groupEntry';
|
||||
|
||||
export class GroupResponse {
|
||||
externalId: string;
|
||||
displayName: string;
|
||||
userIds: string[];
|
||||
|
||||
constructor(g: GroupEntry) {
|
||||
this.externalId = g.externalId;
|
||||
this.displayName = g.displayName;
|
||||
this.userIds = Array.from(g.userMemberExternalIds);
|
||||
}
|
||||
}
|
||||
22
src/models/response/testResponse.ts
Normal file
22
src/models/response/testResponse.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { GroupResponse } from './groupResponse';
|
||||
import { UserResponse } from './userResponse';
|
||||
|
||||
import { SimResult } from '../simResult';
|
||||
|
||||
import { BaseResponse } from 'jslib-node/cli/models/response/baseResponse';
|
||||
|
||||
export class TestResponse implements BaseResponse {
|
||||
object: string;
|
||||
groups: GroupResponse[] = [];
|
||||
enabledUsers: UserResponse[] = [];
|
||||
disabledUsers: UserResponse[] = [];
|
||||
deletedUsers: UserResponse[] = [];
|
||||
|
||||
constructor(result: SimResult) {
|
||||
this.object = 'test';
|
||||
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.disabledUsers = result.disabledUsers != null ? result.disabledUsers.map(u => new UserResponse(u)) : [];
|
||||
this.deletedUsers = result.deletedUsers != null ? result.deletedUsers.map(u => new UserResponse(u)) : [];
|
||||
}
|
||||
}
|
||||
11
src/models/response/userResponse.ts
Normal file
11
src/models/response/userResponse.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { UserEntry } from '../userEntry';
|
||||
|
||||
export class UserResponse {
|
||||
externalId: string;
|
||||
displayName: string;
|
||||
|
||||
constructor(u: UserEntry) {
|
||||
this.externalId = u.externalId;
|
||||
this.displayName = u.displayName;
|
||||
}
|
||||
}
|
||||
10
src/models/simResult.ts
Normal file
10
src/models/simResult.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { GroupEntry } from './groupEntry';
|
||||
import { UserEntry } from './userEntry';
|
||||
|
||||
export class SimResult {
|
||||
groups: GroupEntry[] = [];
|
||||
users: UserEntry[] = [];
|
||||
enabledUsers: UserEntry[] = [];
|
||||
disabledUsers: UserEntry[] = [];
|
||||
deletedUsers: UserEntry[] = [];
|
||||
}
|
||||
@@ -5,6 +5,8 @@ export class SyncConfiguration {
|
||||
userFilter: string;
|
||||
groupFilter: string;
|
||||
removeDisabled = false;
|
||||
overwriteExisting = false;
|
||||
largeImport = false;
|
||||
// Ldap properties
|
||||
groupObjectClass: string;
|
||||
userObjectClass: string;
|
||||
|
||||
303
src/package-lock.json
generated
303
src/package-lock.json
generated
@@ -1,303 +0,0 @@
|
||||
{
|
||||
"name": "bitwarden",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"argparse": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
||||
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
|
||||
"requires": {
|
||||
"sprintf-js": "1.0.3"
|
||||
}
|
||||
},
|
||||
"bluebird": {
|
||||
"version": "3.5.1",
|
||||
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz",
|
||||
"integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA=="
|
||||
},
|
||||
"bluebird-lst": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/bluebird-lst/-/bluebird-lst-1.0.5.tgz",
|
||||
"integrity": "sha512-Ey0bDNys5qpYPhZ/oQ9vOEvD0TYQDTILMXWP2iGfvMg7rSDde+oV4aQQgqRH+CvBFNz2BSDQnPGMUl6LKBUUQA==",
|
||||
"requires": {
|
||||
"bluebird": "3.5.1"
|
||||
}
|
||||
},
|
||||
"builder-util-runtime": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-4.2.0.tgz",
|
||||
"integrity": "sha512-cROCExnJOJvRD58HHcnrrgyRAoDHGZT0hKox0op7vTuuuRC/1JKMXvSR+Hxy7KWy/aEmKu0HfSqMd4znDEqQsA==",
|
||||
"requires": {
|
||||
"bluebird-lst": "1.0.5",
|
||||
"debug": "3.1.0",
|
||||
"fs-extra-p": "4.5.2",
|
||||
"sax": "1.2.4"
|
||||
}
|
||||
},
|
||||
"conf": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/conf/-/conf-1.4.0.tgz",
|
||||
"integrity": "sha512-bzlVWS2THbMetHqXKB8ypsXN4DQ/1qopGwNJi1eYbpwesJcd86FBjFciCQX/YwAhp9bM7NVnPFqZ5LpV7gP0Dg==",
|
||||
"requires": {
|
||||
"dot-prop": "4.2.0",
|
||||
"env-paths": "1.0.0",
|
||||
"make-dir": "1.2.0",
|
||||
"pkg-up": "2.0.0",
|
||||
"write-file-atomic": "2.3.0"
|
||||
}
|
||||
},
|
||||
"debug": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
|
||||
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
|
||||
"requires": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"dot-prop": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz",
|
||||
"integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==",
|
||||
"requires": {
|
||||
"is-obj": "1.0.1"
|
||||
}
|
||||
},
|
||||
"electron-is-dev": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/electron-is-dev/-/electron-is-dev-0.3.0.tgz",
|
||||
"integrity": "sha1-FOb9pcaOnk7L7/nM8DfL18BcWv4="
|
||||
},
|
||||
"electron-log": {
|
||||
"version": "2.2.14",
|
||||
"resolved": "https://registry.npmjs.org/electron-log/-/electron-log-2.2.14.tgz",
|
||||
"integrity": "sha512-Rj+XyK4nShe/nv9v1Uks4KEfjtQ6N+eSnx5CLpAjG6rlyUdAflyFHoybcHSLoq9l9pGavclULWS5IXgk8umc2g=="
|
||||
},
|
||||
"electron-store": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/electron-store/-/electron-store-1.3.0.tgz",
|
||||
"integrity": "sha512-r1Pdl5MwpiCxgbsl0qnwv/GABO5+J/JTO16+KyqL+bOITIk9o3cq3Sw69uO9NgPkpfcKeEwxtJFbtbiBlGTiDA==",
|
||||
"requires": {
|
||||
"conf": "1.4.0"
|
||||
}
|
||||
},
|
||||
"electron-updater": {
|
||||
"version": "2.21.4",
|
||||
"resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-2.21.4.tgz",
|
||||
"integrity": "sha512-x6QSbyxgGR3szIOBtFoCJH0TfgB55AWHaXmilNgorfvpnCdEMQEATxEzLOW0JCzzcB5y3vBrawvmMUEdXwutmA==",
|
||||
"requires": {
|
||||
"bluebird-lst": "1.0.5",
|
||||
"builder-util-runtime": "4.2.0",
|
||||
"electron-is-dev": "0.3.0",
|
||||
"fs-extra-p": "4.5.2",
|
||||
"js-yaml": "3.11.0",
|
||||
"lazy-val": "1.0.3",
|
||||
"lodash.isequal": "4.5.0",
|
||||
"semver": "5.5.0",
|
||||
"source-map-support": "0.5.4"
|
||||
}
|
||||
},
|
||||
"env-paths": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-1.0.0.tgz",
|
||||
"integrity": "sha1-QWgTO0K7BcOKNbGuQ5fIKYqzaeA="
|
||||
},
|
||||
"esprima": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz",
|
||||
"integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw=="
|
||||
},
|
||||
"find-up": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz",
|
||||
"integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=",
|
||||
"requires": {
|
||||
"locate-path": "2.0.0"
|
||||
}
|
||||
},
|
||||
"fs-extra": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-5.0.0.tgz",
|
||||
"integrity": "sha512-66Pm4RYbjzdyeuqudYqhFiNBbCIuI9kgRqLPSHIlXHidW8NIQtVdkM1yeZ4lXwuhbTETv3EUGMNHAAw6hiundQ==",
|
||||
"requires": {
|
||||
"graceful-fs": "4.1.11",
|
||||
"jsonfile": "4.0.0",
|
||||
"universalify": "0.1.1"
|
||||
}
|
||||
},
|
||||
"fs-extra-p": {
|
||||
"version": "4.5.2",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra-p/-/fs-extra-p-4.5.2.tgz",
|
||||
"integrity": "sha512-ZYqFpBdy9w7PsK+vB30j+TnHOyWHm/CJbUq1qqoE8tb71m6qgk5Wa7gp3MYQdlGFxb9vfznF+yD4jcl8l+y91A==",
|
||||
"requires": {
|
||||
"bluebird-lst": "1.0.5",
|
||||
"fs-extra": "5.0.0"
|
||||
}
|
||||
},
|
||||
"graceful-fs": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz",
|
||||
"integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg="
|
||||
},
|
||||
"imurmurhash": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
|
||||
"integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o="
|
||||
},
|
||||
"is-obj": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz",
|
||||
"integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8="
|
||||
},
|
||||
"js-yaml": {
|
||||
"version": "3.11.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.11.0.tgz",
|
||||
"integrity": "sha512-saJstZWv7oNeOyBh3+Dx1qWzhW0+e6/8eDzo7p5rDFqxntSztloLtuKu+Ejhtq82jsilwOIZYsCz+lIjthg1Hw==",
|
||||
"requires": {
|
||||
"argparse": "1.0.10",
|
||||
"esprima": "4.0.0"
|
||||
}
|
||||
},
|
||||
"jsonfile": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
|
||||
"integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
|
||||
"requires": {
|
||||
"graceful-fs": "4.1.11"
|
||||
}
|
||||
},
|
||||
"keytar": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/keytar/-/keytar-4.1.0.tgz",
|
||||
"integrity": "sha512-L3KqiSMtpVitlug4uuI+K5XLne9SAVEFWE8SCQIhQiH0IA/CTbon5v5prVLKK0Ken54o2O8V9HceKagpwJum+Q==",
|
||||
"requires": {
|
||||
"nan": "2.5.1"
|
||||
}
|
||||
},
|
||||
"lazy-val": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.3.tgz",
|
||||
"integrity": "sha512-pjCf3BYk+uv3ZcPzEVM0BFvO9Uw58TmlrU0oG5tTrr9Kcid3+kdKxapH8CjdYmVa2nO5wOoZn2rdvZx2PKj/xg=="
|
||||
},
|
||||
"locate-path": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz",
|
||||
"integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=",
|
||||
"requires": {
|
||||
"p-locate": "2.0.0",
|
||||
"path-exists": "3.0.0"
|
||||
}
|
||||
},
|
||||
"lodash.isequal": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
|
||||
"integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA="
|
||||
},
|
||||
"make-dir": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.2.0.tgz",
|
||||
"integrity": "sha512-aNUAa4UMg/UougV25bbrU4ZaaKNjJ/3/xnvg/twpmKROPdKZPZ9wGgI0opdZzO8q/zUFawoUuixuOv33eZ61Iw==",
|
||||
"requires": {
|
||||
"pify": "3.0.0"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
||||
},
|
||||
"nan": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.5.1.tgz",
|
||||
"integrity": "sha1-1bAWkSUzJql6K77p5hxV2NYDUeI="
|
||||
},
|
||||
"p-limit": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.2.0.tgz",
|
||||
"integrity": "sha512-Y/OtIaXtUPr4/YpMv1pCL5L5ed0rumAaAeBSj12F+bSlMdys7i8oQF/GUJmfpTS/QoaRrS/k6pma29haJpsMng==",
|
||||
"requires": {
|
||||
"p-try": "1.0.0"
|
||||
}
|
||||
},
|
||||
"p-locate": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz",
|
||||
"integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=",
|
||||
"requires": {
|
||||
"p-limit": "1.2.0"
|
||||
}
|
||||
},
|
||||
"p-try": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz",
|
||||
"integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M="
|
||||
},
|
||||
"path-exists": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
|
||||
"integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU="
|
||||
},
|
||||
"pify": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
|
||||
"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY="
|
||||
},
|
||||
"pkg-up": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-2.0.0.tgz",
|
||||
"integrity": "sha1-yBmscoBZpGHKscOImivjxJoATX8=",
|
||||
"requires": {
|
||||
"find-up": "2.1.0"
|
||||
}
|
||||
},
|
||||
"sax": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
|
||||
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
|
||||
},
|
||||
"semver": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz",
|
||||
"integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA=="
|
||||
},
|
||||
"signal-exit": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
|
||||
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0="
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
|
||||
},
|
||||
"source-map-support": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.4.tgz",
|
||||
"integrity": "sha512-PETSPG6BjY1AHs2t64vS2aqAgu6dMIMXJULWFBGbh2Gr8nVLbCFDo6i/RMMvviIQ2h1Z8+5gQhVKSn2je9nmdg==",
|
||||
"requires": {
|
||||
"source-map": "0.6.1"
|
||||
}
|
||||
},
|
||||
"sprintf-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
|
||||
},
|
||||
"universalify": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz",
|
||||
"integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc="
|
||||
},
|
||||
"write-file-atomic": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.3.0.tgz",
|
||||
"integrity": "sha512-xuPeK4OdjWqtfi59ylvVL0Yn35SF3zgcAcv7rBPFHVaEapaDr4GdGgm3j7ckTwH9wHL7fGmgfAnb0+THrHb8tA==",
|
||||
"requires": {
|
||||
"graceful-fs": "4.1.11",
|
||||
"imurmurhash": "0.1.4",
|
||||
"signal-exit": "3.0.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,19 +2,20 @@
|
||||
"name": "bitwarden-directory-connector",
|
||||
"productName": "Bitwarden Directory Connector",
|
||||
"description": "Sync your user directory to your Bitwarden organization.",
|
||||
"version": "2.0.2",
|
||||
"author": "8bit Solutions LLC <hello@bitwarden.com> (https://bitwarden.com)",
|
||||
"version": "2.9.7",
|
||||
"author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)",
|
||||
"homepage": "https://bitwarden.com",
|
||||
"license": "GPL-3.0",
|
||||
"main": "main.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/bitwarden/desktop"
|
||||
"url": "https://github.com/bitwarden/directory-connector"
|
||||
},
|
||||
"dependencies": {
|
||||
"electron-log": "2.2.14",
|
||||
"electron-updater": "2.21.4",
|
||||
"keytar": "4.1.0",
|
||||
"lowdb": "1.0.0"
|
||||
"browser-hrtime": "^1.1.8",
|
||||
"electron-log": "4.3.5",
|
||||
"electron-store": "8.0.0",
|
||||
"electron-updater": "4.3.9",
|
||||
"keytar": "7.6.0"
|
||||
}
|
||||
}
|
||||
|
||||
301
src/program.ts
Normal file
301
src/program.ts
Normal file
@@ -0,0 +1,301 @@
|
||||
import * as chalk from 'chalk';
|
||||
import * as program from 'commander';
|
||||
import * as path from 'path';
|
||||
|
||||
import { Main } from './bwdc';
|
||||
|
||||
import { ClearCacheCommand } from './commands/clearCache.command';
|
||||
import { ConfigCommand } from './commands/config.command';
|
||||
import { LastSyncCommand } from './commands/lastSync.command';
|
||||
import { SyncCommand } from './commands/sync.command';
|
||||
import { TestCommand } from './commands/test.command';
|
||||
|
||||
import { LoginCommand } from 'jslib-node/cli/commands/login.command';
|
||||
import { LogoutCommand } from 'jslib-node/cli/commands/logout.command';
|
||||
import { UpdateCommand } from 'jslib-node/cli/commands/update.command';
|
||||
|
||||
import { BaseProgram } from 'jslib-node/cli/baseProgram';
|
||||
|
||||
import { ApiKeyService } from 'jslib-common/abstractions/apiKey.service';
|
||||
import { Response } from 'jslib-node/cli/models/response';
|
||||
import { StringResponse } from 'jslib-node/cli/models/response/stringResponse';
|
||||
|
||||
import { Utils } from 'jslib-common/misc/utils';
|
||||
|
||||
const writeLn = (s: string, finalLine: boolean = false, error: boolean = false) => {
|
||||
const stream = error ? process.stderr : process.stdout;
|
||||
if (finalLine && process.platform === 'win32') {
|
||||
stream.write(s);
|
||||
} else {
|
||||
stream.write(s + '\n');
|
||||
}
|
||||
};
|
||||
|
||||
export class Program extends BaseProgram {
|
||||
private apiKeyService: ApiKeyService;
|
||||
|
||||
constructor(private main: Main) {
|
||||
super(main.userService, writeLn);
|
||||
this.apiKeyService = main.apiKeyService;
|
||||
}
|
||||
|
||||
async run() {
|
||||
program
|
||||
.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', () => {
|
||||
process.env.BW_PRETTY = 'true';
|
||||
});
|
||||
|
||||
program.on('option:raw', () => {
|
||||
process.env.BW_RAW = 'true';
|
||||
});
|
||||
|
||||
program.on('option:quiet', () => {
|
||||
process.env.BW_QUIET = 'true';
|
||||
});
|
||||
|
||||
program.on('option:response', () => {
|
||||
process.env.BW_RESPONSE = 'true';
|
||||
});
|
||||
|
||||
program.on('option:cleanexit', () => {
|
||||
process.env.BW_CLEANEXIT = 'true';
|
||||
});
|
||||
|
||||
program.on('option:nointeraction', () => {
|
||||
process.env.BW_NOINTERACTION = 'true';
|
||||
});
|
||||
|
||||
program.on('command:*', () => {
|
||||
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('--help', () => {
|
||||
writeLn('\n Examples:');
|
||||
writeLn('');
|
||||
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
|
||||
.command('login [clientId] [clientSecret]')
|
||||
.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();
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
const authed = await this.apiKeyService.isAuthenticated();
|
||||
if (!authed) {
|
||||
this.processResponse(Response.error('You are not logged in.'), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
src/scss/bootstrap.scss
vendored
11
src/scss/bootstrap.scss
vendored
@@ -1,4 +1,4 @@
|
||||
$theme-colors: ( "primary": #3c8dbc, "primary-accent": #286090, "danger": #dd4b39, "success": #00a65a, "info": #555555, "warning": #bf7e16);
|
||||
$theme-colors: ( "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;
|
||||
@@ -8,4 +8,13 @@ $h4-font-size: 1rem;
|
||||
$h5-font-size: 1rem;
|
||||
$h6-font-size: 1rem;
|
||||
|
||||
$primary: map_get($theme-colors, 'primary');
|
||||
$primary-accent: map_get($theme-colors, 'primary-accent');
|
||||
$success: map_get($theme-colors, 'success');
|
||||
$info: map_get($theme-colors, 'info');
|
||||
$warning: map_get($theme-colors, 'warning');
|
||||
$danger: map_get($theme-colors, 'danger');
|
||||
$secondary: map_get($theme-colors, 'secondary');
|
||||
$secondary-alt: map_get($theme-colors, 'secondary-alt');
|
||||
|
||||
@import "~bootstrap/scss/bootstrap.scss";
|
||||
|
||||
@@ -53,3 +53,92 @@ ul.testing-list {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
}
|
||||
|
||||
.callout {
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
border: 1px solid #000000;
|
||||
border-left-width: 5px;
|
||||
border-radius: 3px;
|
||||
border-color: #ddd;
|
||||
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 {
|
||||
color: $primary;
|
||||
}
|
||||
}
|
||||
|
||||
&.callout-info {
|
||||
border-left-color: $info;
|
||||
|
||||
.callout-heading {
|
||||
color: $info;
|
||||
}
|
||||
}
|
||||
|
||||
&.callout-danger {
|
||||
border-left-color: $danger;
|
||||
|
||||
.callout-heading {
|
||||
color: $danger;
|
||||
}
|
||||
}
|
||||
|
||||
&.callout-success {
|
||||
border-left-color: $success;
|
||||
|
||||
.callout-heading {
|
||||
color: $success;
|
||||
}
|
||||
}
|
||||
|
||||
&.callout-warning {
|
||||
border-left-color: $warning;
|
||||
|
||||
.callout-heading {
|
||||
color: $warning;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-left: 40px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.btn[class*="btn-outline-"] {
|
||||
&:not(:hover) {
|
||||
border-color: $secondary;
|
||||
background-color: #fbfbfb;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-outline-secondary {
|
||||
color: $text-muted;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
color: $body-color;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&.focus {
|
||||
box-shadow: 0 0 0 $btn-focus-width rgba(mix(color-yiq($primary), $primary, 15%), .5);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,16 @@ $fa-font-path: "~font-awesome/fonts";
|
||||
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;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@import "bootstrap.scss";
|
||||
@import "../../jslib/angular/src/scss/webfonts.css";
|
||||
@import "bootstrap.scss";
|
||||
@import "pages.scss";
|
||||
@import "misc.scss";
|
||||
@import "plugins.scss";
|
||||
|
||||
31
src/services/api.service.ts
Normal file
31
src/services/api.service.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { ApiKeyService } from 'jslib-common/abstractions/apiKey.service';
|
||||
import { AuthService } from 'jslib-common/abstractions/auth.service';
|
||||
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 { ApiService as ApiServiceBase } from 'jslib-common/services/api.service';
|
||||
|
||||
export async function refreshToken(apiKeyService: ApiKeyService, authService: AuthService) {
|
||||
try {
|
||||
const clientId = await apiKeyService.getClientId();
|
||||
const clientSecret = await apiKeyService.getClientSecret();
|
||||
if (clientId != null && clientSecret != null) {
|
||||
await authService.logInApiKey(clientId, clientSecret);
|
||||
}
|
||||
} catch (e) {
|
||||
return Promise.reject(e);
|
||||
}
|
||||
}
|
||||
|
||||
export class ApiService extends ApiServiceBase {
|
||||
constructor(tokenService: TokenService, platformUtilsService: PlatformUtilsService, environmentService: EnvironmentService,
|
||||
private refreshTokenCallback: () => Promise<void>, logoutCallback: (expired: boolean) => Promise<void>,
|
||||
customUserAgent: string = null) {
|
||||
super(tokenService, platformUtilsService, environmentService, logoutCallback, customUserAgent);
|
||||
}
|
||||
|
||||
doRefreshToken(): Promise<void> {
|
||||
return this.refreshTokenCallback();
|
||||
}
|
||||
}
|
||||
61
src/services/auth.service.ts
Normal file
61
src/services/auth.service.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
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 { CryptoService } from 'jslib-common/abstractions/crypto.service';
|
||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
||||
import { LogService } from 'jslib-common/abstractions/log.service';
|
||||
import { MessagingService } from 'jslib-common/abstractions/messaging.service';
|
||||
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
|
||||
import { TokenService } from 'jslib-common/abstractions/token.service';
|
||||
import { UserService } from 'jslib-common/abstractions/user.service';
|
||||
import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.service';
|
||||
|
||||
import { AuthService as AuthServiceBase } from 'jslib-common/services/auth.service';
|
||||
|
||||
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 {
|
||||
|
||||
constructor(cryptoService: CryptoService, apiService: ApiService, userService: UserService,
|
||||
tokenService: TokenService, appIdService: AppIdService, i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService, messagingService: MessagingService,
|
||||
vaultTimeoutService: VaultTimeoutService, logService: LogService, private apiKeyService: ApiKeyService,
|
||||
setCryptoKeys = true) {
|
||||
super(cryptoService, apiService, userService, tokenService, appIdService, i18nService, platformUtilsService,
|
||||
messagingService, vaultTimeoutService, logService, setCryptoKeys);
|
||||
}
|
||||
|
||||
async logInApiKey(clientId: string, clientSecret: string): Promise<AuthResult> {
|
||||
this.selectedTwoFactorProviderType = null;
|
||||
if (clientId.startsWith('organization')) {
|
||||
return await this.organizationLogInHelper(clientId, clientSecret);
|
||||
}
|
||||
return await super.logInApiKey(clientId, clientSecret);
|
||||
}
|
||||
|
||||
async logOut(callback: Function) {
|
||||
this.apiKeyService.clear();
|
||||
super.logOut(callback);
|
||||
}
|
||||
|
||||
private async organizationLogInHelper(clientId: string, clientSecret: string) {
|
||||
const appId = await this.appIdService.getAppId();
|
||||
const deviceRequest = new DeviceRequest(appId, this.platformUtilsService);
|
||||
const request = new TokenRequest(null, null, [clientId, clientSecret], null,
|
||||
null, false, null, deviceRequest);
|
||||
|
||||
const response = await this.apiService.postIdentityToken(request);
|
||||
const result = new AuthResult();
|
||||
result.twoFactor = !(response as any).accessToken;
|
||||
|
||||
const tokenResponse = response as IdentityTokenResponse;
|
||||
result.resetMasterPassword = tokenResponse.resetMasterPassword;
|
||||
await this.tokenService.setToken(tokenResponse.accessToken);
|
||||
await this.apiKeyService.setInformation(clientId, clientSecret);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -12,16 +12,24 @@ import { UserEntry } from '../models/userEntry';
|
||||
|
||||
import { BaseDirectoryService } from './baseDirectory.service';
|
||||
import { ConfigurationService } from './configuration.service';
|
||||
import { DirectoryService } from './directory.service';
|
||||
import { IDirectoryService } from './directory.service';
|
||||
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { LogService } from 'jslib/abstractions/log.service';
|
||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
||||
import { LogService } from 'jslib-common/abstractions/log.service';
|
||||
|
||||
const NextLink = '@odata.nextLink';
|
||||
const DeltaLink = '@odata.deltaLink';
|
||||
const ObjectType = '@odata.type';
|
||||
const UserSelectParams = '?$select=id,mail,userPrincipalName,displayName,accountEnabled';
|
||||
|
||||
export class AzureDirectoryService extends BaseDirectoryService implements DirectoryService {
|
||||
enum UserSetType {
|
||||
IncludeUser,
|
||||
ExcludeUser,
|
||||
IncludeGroup,
|
||||
ExcludeGroup,
|
||||
}
|
||||
|
||||
export class AzureDirectoryService extends BaseDirectoryService implements IDirectoryService {
|
||||
private client: graph.Client;
|
||||
private dirConfig: AzureConfiguration;
|
||||
private syncConfig: SyncConfiguration;
|
||||
@@ -53,20 +61,62 @@ export class AzureDirectoryService extends BaseDirectoryService implements Direc
|
||||
|
||||
let users: UserEntry[];
|
||||
if (this.syncConfig.users) {
|
||||
users = await this.getUsers(force, !test);
|
||||
users = await this.getCurrentUsers();
|
||||
const deletedUsers = await this.getDeletedUsers(force, !test);
|
||||
users = users.concat(deletedUsers);
|
||||
}
|
||||
|
||||
let groups: GroupEntry[];
|
||||
if (this.syncConfig.groups) {
|
||||
const setFilter = this.createCustomSet(this.syncConfig.groupFilter);
|
||||
groups = await this.getGroups(this.forceGroup(force, users), !test, setFilter);
|
||||
users = this.filterUsersFromGroupsSet(users, groups, setFilter);
|
||||
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 getUsers(force: boolean, saveDelta: boolean): Promise<UserEntry[]> {
|
||||
private async getCurrentUsers(): Promise<UserEntry[]> {
|
||||
const entryIds = new Set<string>();
|
||||
const entries: UserEntry[] = [];
|
||||
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[]> {
|
||||
const entryIds = new Set<string>();
|
||||
const entries: UserEntry[] = [];
|
||||
|
||||
let res: any = null;
|
||||
@@ -81,26 +131,28 @@ export class AzureDirectoryService extends BaseDirectoryService implements Direc
|
||||
}
|
||||
|
||||
if (res == null) {
|
||||
const userReq = this.client.api('/users/delta');
|
||||
const userReq = this.client.api('/users/delta' + UserSelectParams);
|
||||
res = await userReq.get();
|
||||
}
|
||||
|
||||
const setFilter = this.createCustomSet(this.syncConfig.userFilter);
|
||||
const setFilter = this.createCustomUserSet(this.syncConfig.userFilter);
|
||||
while (true) {
|
||||
const users: graphType.User[] = res.value;
|
||||
if (users != null) {
|
||||
for (const user of users) {
|
||||
const entry = this.buildUser(user);
|
||||
if (this.filterOutResult(setFilter, entry.email)) {
|
||||
if (user.id == null || entryIds.has(user.id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!entry.disabled && !entry.deleted &&
|
||||
(entry.email == null || entry.email.indexOf('#') > -1)) {
|
||||
const entry = this.buildUser(user);
|
||||
if (!entry.deleted) {
|
||||
continue;
|
||||
}
|
||||
if (await this.filterOutUserResult(setFilter, entry, false)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
entries.push(entry);
|
||||
entryIds.add(user.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,11 +170,145 @@ export class AzureDirectoryService extends BaseDirectoryService implements Direc
|
||||
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,
|
||||
checkGroupsFilter: boolean): 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);
|
||||
}
|
||||
|
||||
// We need to *not* call the /checkMemberGroups method for deleted users, it will always fail
|
||||
if (!checkGroupsFilter) {
|
||||
return false;
|
||||
}
|
||||
const memberGroups = await this.client.api(`/users/${user.externalId}/checkMemberGroups`).post({
|
||||
groupIds: Array.from(setFilter[1]),
|
||||
});
|
||||
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;
|
||||
}
|
||||
|
||||
private buildUser(user: graphType.User): UserEntry {
|
||||
const entry = new UserEntry();
|
||||
entry.referenceId = user.id;
|
||||
entry.externalId = user.id;
|
||||
entry.email = user.mail || user.userPrincipalName;
|
||||
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') {
|
||||
@@ -132,77 +318,25 @@ export class AzureDirectoryService extends BaseDirectoryService implements Direc
|
||||
return entry;
|
||||
}
|
||||
|
||||
private async getGroups(force: boolean, saveDelta: boolean,
|
||||
setFilter: [boolean, Set<string>]): Promise<GroupEntry[]> {
|
||||
private async getGroups(setFilter: [boolean, Set<string>]): Promise<GroupEntry[]> {
|
||||
const entryIds = new Set<string>();
|
||||
const entries: GroupEntry[] = [];
|
||||
const changedGroupIds: string[] = [];
|
||||
const token = await this.configurationService.getGroupDeltaToken();
|
||||
const getFullResults = token == null || force;
|
||||
let res: any = null;
|
||||
let errored = false;
|
||||
|
||||
try {
|
||||
if (!getFullResults) {
|
||||
try {
|
||||
const deltaReq = this.client.api(token);
|
||||
res = await deltaReq.get();
|
||||
} catch {
|
||||
res = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (res == null) {
|
||||
const groupReq = this.client.api('/groups/delta');
|
||||
res = await groupReq.get();
|
||||
}
|
||||
|
||||
while (true) {
|
||||
const groups: graphType.Group[] = res.value;
|
||||
if (groups != null) {
|
||||
for (const group of groups) {
|
||||
if (getFullResults) {
|
||||
if (this.filterOutResult(setFilter, group.displayName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const entry = await this.buildGroup(group);
|
||||
entries.push(entry);
|
||||
} else {
|
||||
changedGroupIds.push(group.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (res[NextLink] == null) {
|
||||
if (res[DeltaLink] != null && saveDelta) {
|
||||
await this.configurationService.saveGroupDeltaToken(res[DeltaLink]);
|
||||
}
|
||||
break;
|
||||
} else {
|
||||
const nextReq = this.client.api(res[NextLink]);
|
||||
res = await nextReq.get();
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
errored = true;
|
||||
}
|
||||
|
||||
if (!errored && (getFullResults || changedGroupIds.length === 0)) {
|
||||
return entries;
|
||||
}
|
||||
|
||||
const allGroupsReq = this.client.api('/groups');
|
||||
res = await allGroupsReq.get();
|
||||
const groupsReq = this.client.api('/groups');
|
||||
let res = await groupsReq.get();
|
||||
while (true) {
|
||||
const allGroups: graphType.Group[] = res.value;
|
||||
if (allGroups != null) {
|
||||
for (const group of allGroups) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,16 +358,24 @@ export class AzureDirectoryService extends BaseDirectoryService implements Direc
|
||||
entry.name = group.displayName;
|
||||
|
||||
const memReq = this.client.api('/groups/' + group.id + '/members');
|
||||
const memRes = await memReq.get();
|
||||
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);
|
||||
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;
|
||||
@@ -241,10 +383,10 @@ export class AzureDirectoryService extends BaseDirectoryService implements Direc
|
||||
|
||||
private init() {
|
||||
this.client = graph.Client.init({
|
||||
authProvider: (done) => {
|
||||
authProvider: done => {
|
||||
if (this.dirConfig.applicationId == null || this.dirConfig.key == null ||
|
||||
this.dirConfig.tenant == null) {
|
||||
done(this.i18nService.t('dirConfigIncomplete'), null);
|
||||
done(new Error(this.i18nService.t('dirConfigIncomplete')), null);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -271,7 +413,7 @@ export class AzureDirectoryService extends BaseDirectoryService implements Direc
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Content-Length': Buffer.byteLength(data),
|
||||
},
|
||||
}, (res) => {
|
||||
}, res => {
|
||||
res.setEncoding('utf8');
|
||||
res.on('data', (chunk: string) => {
|
||||
const d = JSON.parse(chunk);
|
||||
@@ -279,12 +421,17 @@ export class AzureDirectoryService extends BaseDirectoryService implements Direc
|
||||
this.setAccessTokenExpiration(d.access_token, d.expires_in);
|
||||
done(null, d.access_token);
|
||||
} else if (d.error != null && d.error_description != null) {
|
||||
done(d.error + ' (' + res.statusCode + '): ' + 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 {
|
||||
done('Unknown error (' + res.statusCode + ').', null);
|
||||
const err = new Error('Unknown error (' + res.statusCode + ').');
|
||||
done(err, null);
|
||||
}
|
||||
});
|
||||
}).on('error', (err) => {
|
||||
}).on('error', err => {
|
||||
done(err, null);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { SyncConfiguration } from '../models/syncConfiguration';
|
||||
|
||||
import { GroupEntry } from '../models/groupEntry';
|
||||
import { UserEntry } from '../models/userEntry';
|
||||
|
||||
@@ -51,13 +53,13 @@ export abstract class BaseDirectoryService {
|
||||
|
||||
protected filterOutResult(setFilter: [boolean, Set<string>], result: string) {
|
||||
if (setFilter != null) {
|
||||
result = result.trim().toLowerCase();
|
||||
const cleanResult = result != null ? result.trim().toLowerCase() : '--';
|
||||
const excluded = setFilter[0];
|
||||
const set = setFilter[1];
|
||||
|
||||
if (excluded && set.has(result)) {
|
||||
if (excluded && set.has(cleanResult)) {
|
||||
return true;
|
||||
} else if (!excluded && !set.has(result)) {
|
||||
} else if (!excluded && !set.has(cleanResult)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -66,21 +68,24 @@ export abstract class BaseDirectoryService {
|
||||
}
|
||||
|
||||
protected filterUsersFromGroupsSet(users: UserEntry[], groups: GroupEntry[],
|
||||
setFilter: [boolean, Set<string>]): UserEntry[] {
|
||||
setFilter: [boolean, Set<string>], syncConfig: SyncConfiguration): UserEntry[] {
|
||||
if (setFilter == null || users == null) {
|
||||
return users;
|
||||
}
|
||||
|
||||
return users.filter((u) => {
|
||||
if (u.disabled || u.deleted) {
|
||||
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;
|
||||
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);
|
||||
return force || (users != null && users.filter(u => !u.deleted && !u.disabled).length > 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { DirectoryType } from '../enums/directoryType';
|
||||
|
||||
import { StorageService } from 'jslib/abstractions/storage.service';
|
||||
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]';
|
||||
@@ -13,6 +14,7 @@ const Keys = {
|
||||
gsuite: 'gsuitePrivateKey',
|
||||
azure: 'azureKey',
|
||||
okta: 'oktaToken',
|
||||
oneLogin: 'oneLoginClientSecret',
|
||||
directoryConfigPrefix: 'directoryConfig_',
|
||||
sync: 'syncConfig',
|
||||
directoryType: 'directoryType',
|
||||
@@ -25,7 +27,8 @@ const Keys = {
|
||||
};
|
||||
|
||||
export class ConfigurationService {
|
||||
constructor(private storageService: StorageService, private secureStorageService: StorageService) { }
|
||||
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);
|
||||
@@ -33,61 +36,77 @@ export class ConfigurationService {
|
||||
return config;
|
||||
}
|
||||
|
||||
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;
|
||||
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): Promise<any> {
|
||||
config: LdapConfiguration | GSuiteConfiguration | AzureConfiguration | OktaConfiguration |
|
||||
OneLoginConfiguration): Promise<any> {
|
||||
const savedConfig: any = Object.assign({}, config);
|
||||
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;
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { GroupEntry } from '../models/groupEntry';
|
||||
import { UserEntry } from '../models/userEntry';
|
||||
|
||||
export interface DirectoryService {
|
||||
export interface IDirectoryService {
|
||||
getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]>;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
import { JWT } from 'google-auth-library';
|
||||
import {
|
||||
admin_directory_v1,
|
||||
google,
|
||||
GoogleApis,
|
||||
} from 'googleapis';
|
||||
import {
|
||||
Admin,
|
||||
Schema$Group,
|
||||
Schema$User,
|
||||
} from 'googleapis/build/src/apis/admin/directory_v1';
|
||||
|
||||
import { DirectoryType } from '../enums/directoryType';
|
||||
|
||||
@@ -18,14 +13,14 @@ import { UserEntry } from '../models/userEntry';
|
||||
|
||||
import { BaseDirectoryService } from './baseDirectory.service';
|
||||
import { ConfigurationService } from './configuration.service';
|
||||
import { DirectoryService } from './directory.service';
|
||||
import { IDirectoryService } from './directory.service';
|
||||
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { LogService } from 'jslib/abstractions/log.service';
|
||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
||||
import { LogService } from 'jslib-common/abstractions/log.service';
|
||||
|
||||
export class GSuiteDirectoryService extends BaseDirectoryService implements DirectoryService {
|
||||
export class GSuiteDirectoryService extends BaseDirectoryService implements IDirectoryService {
|
||||
private client: JWT;
|
||||
private service: Admin;
|
||||
private service: admin_directory_v1.Admin;
|
||||
private authParams: any;
|
||||
private dirConfig: GSuiteConfiguration;
|
||||
private syncConfig: SyncConfiguration;
|
||||
@@ -33,7 +28,7 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements Dire
|
||||
constructor(private configurationService: ConfigurationService, private logService: LogService,
|
||||
private i18nService: I18nService) {
|
||||
super();
|
||||
this.service = google.admin<Admin>('directory_v1');
|
||||
this.service = google.admin('directory_v1');
|
||||
}
|
||||
|
||||
async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> {
|
||||
@@ -54,7 +49,7 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements Dire
|
||||
|
||||
await this.auth();
|
||||
|
||||
let users: UserEntry[];
|
||||
let users: UserEntry[] = [];
|
||||
if (this.syncConfig.users) {
|
||||
users = await this.getUsers();
|
||||
}
|
||||
@@ -62,8 +57,8 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements Dire
|
||||
let groups: GroupEntry[];
|
||||
if (this.syncConfig.groups) {
|
||||
const setFilter = this.createCustomSet(this.syncConfig.groupFilter);
|
||||
groups = await this.getGroups(setFilter);
|
||||
users = this.filterUsersFromGroupsSet(users, groups, setFilter);
|
||||
groups = await this.getGroups(setFilter, users);
|
||||
users = this.filterUsersFromGroupsSet(users, groups, setFilter, this.syncConfig);
|
||||
}
|
||||
|
||||
return [groups, users];
|
||||
@@ -72,52 +67,66 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements Dire
|
||||
private async getUsers(): Promise<UserEntry[]> {
|
||||
const entries: UserEntry[] = [];
|
||||
const query = this.createDirectoryQuery(this.syncConfig.userFilter);
|
||||
|
||||
this.logService.info('Querying users.');
|
||||
let p = Object.assign({ query: query }, this.authParams);
|
||||
const res = await this.service.users.list(p);
|
||||
if (res.status !== 200) {
|
||||
throw new Error('User list API failed: ' + res.statusText);
|
||||
}
|
||||
let nextPageToken: string = null;
|
||||
|
||||
const filter = this.createCustomSet(this.syncConfig.userFilter);
|
||||
if (res.data.users != null) {
|
||||
for (const user of res.data.users) {
|
||||
if (this.filterOutResult(filter, user.primaryEmail)) {
|
||||
continue;
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
const entry = this.buildUser(user, false);
|
||||
if (entry != null) {
|
||||
entries.push(entry);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
this.logService.info('Querying deleted users.');
|
||||
p = Object.assign({ showDeleted: true, query: query }, 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 = 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);
|
||||
}
|
||||
|
||||
if (delRes.data.users != null) {
|
||||
for (const user of delRes.data.users) {
|
||||
if (this.filterOutResult(filter, user.primaryEmail)) {
|
||||
continue;
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const entry = this.buildUser(user, true);
|
||||
if (entry != null) {
|
||||
entries.push(entry);
|
||||
}
|
||||
if (nextPageToken == null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
private buildUser(user: Schema$User, deleted: boolean) {
|
||||
private buildUser(user: admin_directory_v1.Schema$User, deleted: boolean) {
|
||||
if ((user.emails == null || user.emails === '') && !deleted) {
|
||||
return null;
|
||||
}
|
||||
@@ -125,59 +134,82 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements Dire
|
||||
const entry = new UserEntry();
|
||||
entry.referenceId = user.id;
|
||||
entry.externalId = user.id;
|
||||
entry.email = user.primaryEmail;
|
||||
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>]): Promise<GroupEntry[]> {
|
||||
private async getGroups(setFilter: [boolean, Set<string>], users: UserEntry[]): Promise<GroupEntry[]> {
|
||||
const entries: GroupEntry[] = [];
|
||||
let nextPageToken: string = null;
|
||||
|
||||
this.logService.info('Querying groups.');
|
||||
const res = await this.service.groups.list(this.authParams);
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Group list API failed: ' + res.statusText);
|
||||
}
|
||||
if (res.data.groups != null) {
|
||||
for (const group of res.data.groups) {
|
||||
if (!this.filterOutResult(setFilter, group.name)) {
|
||||
const entry = await this.buildGroup(group);
|
||||
entries.push(entry);
|
||||
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: Schema$Group) {
|
||||
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;
|
||||
|
||||
const p = Object.assign({ groupKey: group.id }, 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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
if (memRes.data.members != null) {
|
||||
for (const member of memRes.data.members) {
|
||||
if (member.role.toLowerCase() !== 'member') {
|
||||
continue;
|
||||
}
|
||||
if (member.status.toLowerCase() !== 'active') {
|
||||
continue;
|
||||
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 (member.type.toLowerCase() === 'user') {
|
||||
entry.userMemberExternalIds.add(member.id);
|
||||
} else if (member.type.toLowerCase() === 'group') {
|
||||
entry.groupMemberReferenceIds.add(member.id);
|
||||
}
|
||||
if (nextPageToken == null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,7 +224,7 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements Dire
|
||||
|
||||
this.client = new google.auth.JWT({
|
||||
email: this.dirConfig.clientEmail,
|
||||
key: this.dirConfig.privateKey,
|
||||
key: this.dirConfig.privateKey != null ? this.dirConfig.privateKey.trimLeft() : null,
|
||||
subject: this.dirConfig.adminUser,
|
||||
scopes: [
|
||||
'https://www.googleapis.com/auth/admin.directory.user.readonly',
|
||||
@@ -205,9 +237,11 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements Dire
|
||||
|
||||
this.authParams = {
|
||||
auth: this.client,
|
||||
domain: this.dirConfig.domain,
|
||||
};
|
||||
if (this.dirConfig.customer != null) {
|
||||
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,7 +1,7 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import { I18nService as BaseI18nService } from 'jslib/services/i18n.service';
|
||||
import { I18nService as BaseI18nService } from 'jslib-common/services/i18n.service';
|
||||
|
||||
export class I18nService extends BaseI18nService {
|
||||
constructor(systemLanguage: string, localesDirectory: string) {
|
||||
|
||||
29
src/services/keytarSecureStorage.service.ts
Normal file
29
src/services/keytarSecureStorage.service.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import {
|
||||
deletePassword,
|
||||
getPassword,
|
||||
setPassword,
|
||||
} from 'keytar';
|
||||
|
||||
import { StorageService } from 'jslib-common/abstractions/storage.service';
|
||||
|
||||
export class KeytarSecureStorageService implements StorageService {
|
||||
constructor(private serviceName: string) { }
|
||||
|
||||
get<T>(key: string): Promise<T> {
|
||||
return getPassword(this.serviceName, key).then(val => {
|
||||
return JSON.parse(val) as T;
|
||||
});
|
||||
}
|
||||
|
||||
async has(key: string): Promise<boolean> {
|
||||
return (await this.get(key)) != null;
|
||||
}
|
||||
|
||||
save(key: string, obj: any): Promise<any> {
|
||||
return setPassword(this.serviceName, key, JSON.stringify(obj));
|
||||
}
|
||||
|
||||
remove(key: string): Promise<any> {
|
||||
return deletePassword(this.serviceName, key);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
import * as fs from 'fs';
|
||||
import * as ldap from 'ldapjs';
|
||||
|
||||
import { checkServerIdentity, PeerCertificate } from 'tls';
|
||||
|
||||
import { DirectoryType } from '../enums/directoryType';
|
||||
|
||||
import { GroupEntry } from '../models/groupEntry';
|
||||
@@ -8,16 +11,16 @@ import { SyncConfiguration } from '../models/syncConfiguration';
|
||||
import { UserEntry } from '../models/userEntry';
|
||||
|
||||
import { ConfigurationService } from './configuration.service';
|
||||
import { DirectoryService } from './directory.service';
|
||||
import { IDirectoryService } from './directory.service';
|
||||
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { LogService } from 'jslib/abstractions/log.service';
|
||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
||||
import { LogService } from 'jslib-common/abstractions/log.service';
|
||||
|
||||
import { Utils } from 'jslib/misc/utils';
|
||||
import { Utils } from 'jslib-common/misc/utils';
|
||||
|
||||
const UserControlAccountDisabled = 2;
|
||||
|
||||
export class LdapDirectoryService implements DirectoryService {
|
||||
export class LdapDirectoryService implements IDirectoryService {
|
||||
private client: ldap.Client;
|
||||
private dirConfig: LdapConfiguration;
|
||||
private syncConfig: SyncConfiguration;
|
||||
@@ -52,7 +55,7 @@ export class LdapDirectoryService implements DirectoryService {
|
||||
if (this.syncConfig.groups) {
|
||||
let groupForce = force;
|
||||
if (!groupForce && users != null) {
|
||||
const activeUsers = users.filter((u) => !u.deleted && !u.disabled);
|
||||
const activeUsers = users.filter(u => !u.deleted && !u.disabled);
|
||||
groupForce = activeUsers.length > 0;
|
||||
}
|
||||
groups = await this.getGroups(groupForce);
|
||||
@@ -108,10 +111,14 @@ export class LdapDirectoryService implements DirectoryService {
|
||||
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).toLowerCase();
|
||||
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;
|
||||
}
|
||||
@@ -226,6 +233,9 @@ export class LdapDirectoryService implements DirectoryService {
|
||||
}
|
||||
|
||||
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='));
|
||||
@@ -285,7 +295,7 @@ export class LdapDirectoryService implements DirectoryService {
|
||||
const options: ldap.SearchOptions = {
|
||||
filter: filter,
|
||||
scope: 'sub',
|
||||
paged: true,
|
||||
paged: this.dirConfig.pagedSearch,
|
||||
};
|
||||
const entries: T[] = [];
|
||||
return new Promise<T[]>((resolve, reject) => {
|
||||
@@ -295,18 +305,18 @@ export class LdapDirectoryService implements DirectoryService {
|
||||
return;
|
||||
}
|
||||
|
||||
res.on('error', (resErr) => {
|
||||
res.on('error', resErr => {
|
||||
reject(resErr);
|
||||
});
|
||||
|
||||
res.on('searchEntry', (entry) => {
|
||||
res.on('searchEntry', entry => {
|
||||
const e = processEntry(entry);
|
||||
if (e != null) {
|
||||
entries.push(e);
|
||||
}
|
||||
});
|
||||
|
||||
res.on('end', (result) => {
|
||||
res.on('end', result => {
|
||||
resolve(entries);
|
||||
});
|
||||
});
|
||||
@@ -314,18 +324,48 @@ export class LdapDirectoryService implements DirectoryService {
|
||||
}
|
||||
|
||||
private async bind(): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if (this.dirConfig.hostname == null || this.dirConfig.port == null) {
|
||||
reject(this.i18nService.t('dirConfigIncomplete'));
|
||||
return;
|
||||
}
|
||||
|
||||
const url = 'ldap' + (this.dirConfig.ssl ? 's' : '') + '://' + this.dirConfig.hostname +
|
||||
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(),
|
||||
};
|
||||
|
||||
this.client = ldap.createClient({
|
||||
url: url.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;
|
||||
@@ -337,19 +377,35 @@ export class LdapDirectoryService implements DirectoryService {
|
||||
return;
|
||||
}
|
||||
|
||||
this.client.bind(user, pass, (err) => {
|
||||
if (err != null) {
|
||||
reject(err.message);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
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<any> {
|
||||
private async unbind(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.client.unbind((err) => {
|
||||
this.client.unbind(err => {
|
||||
if (err != null) {
|
||||
reject(err);
|
||||
} else {
|
||||
@@ -370,4 +426,23 @@ export class LdapDirectoryService implements DirectoryService {
|
||||
'-' + 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);
|
||||
}
|
||||
}
|
||||
|
||||
28
src/services/lowdbStorage.service.ts
Normal file
28
src/services/lowdbStorage.service.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import * as lock from 'proper-lockfile';
|
||||
|
||||
import { LogService } from 'jslib-common/abstractions/log.service';
|
||||
|
||||
import { LowdbStorageService as LowdbStorageServiceBase } from 'jslib-node/services/lowdbStorage.service';
|
||||
|
||||
import { Utils } from 'jslib-common/misc/utils';
|
||||
|
||||
export class LowdbStorageService extends LowdbStorageServiceBase {
|
||||
constructor(logService: LogService, defaults?: any, dir?: string, allowCache = false, private requireLock = false) {
|
||||
super(logService, defaults, dir, allowCache);
|
||||
}
|
||||
|
||||
protected async lockDbFile<T>(action: () => T): Promise<T> {
|
||||
if (this.requireLock && !Utils.isNullOrWhitespace(this.dataFilePath)) {
|
||||
this.logService.info('acquiring db file lock');
|
||||
return await lock.lock(this.dataFilePath, { retries: 3 }).then(release => {
|
||||
try {
|
||||
return action();
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return action();
|
||||
}
|
||||
}
|
||||
}
|
||||
17
src/services/nodeApi.service.ts
Normal file
17
src/services/nodeApi.service.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -7,18 +7,19 @@ import { UserEntry } from '../models/userEntry';
|
||||
|
||||
import { BaseDirectoryService } from './baseDirectory.service';
|
||||
import { ConfigurationService } from './configuration.service';
|
||||
import { DirectoryService } from './directory.service';
|
||||
import { IDirectoryService } from './directory.service';
|
||||
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { LogService } from 'jslib/abstractions/log.service';
|
||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
||||
import { LogService } from 'jslib-common/abstractions/log.service';
|
||||
|
||||
// tslint:disable-next-line
|
||||
const okta = require('@okta/okta-sdk-nodejs');
|
||||
import * as https from 'https';
|
||||
|
||||
export class OktaDirectoryService extends BaseDirectoryService implements DirectoryService {
|
||||
const DelayBetweenBuildGroupCallsInMilliseconds = 500;
|
||||
|
||||
export class OktaDirectoryService extends BaseDirectoryService implements IDirectoryService {
|
||||
private dirConfig: OktaConfiguration;
|
||||
private syncConfig: SyncConfiguration;
|
||||
private client: any;
|
||||
private lastBuildGroupCall: number;
|
||||
|
||||
constructor(private configurationService: ConfigurationService, private logService: LogService,
|
||||
private i18nService: I18nService) {
|
||||
@@ -45,11 +46,6 @@ export class OktaDirectoryService extends BaseDirectoryService implements Direct
|
||||
throw new Error(this.i18nService.t('dirConfigIncomplete'));
|
||||
}
|
||||
|
||||
this.client = new okta.Client({
|
||||
orgUrl: this.dirConfig.orgUrl,
|
||||
token: this.dirConfig.token,
|
||||
});
|
||||
|
||||
let users: UserEntry[];
|
||||
if (this.syncConfig.users) {
|
||||
users = await this.getUsers(force);
|
||||
@@ -59,7 +55,7 @@ export class OktaDirectoryService extends BaseDirectoryService implements Direct
|
||||
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);
|
||||
users = this.filterUsersFromGroupsSet(users, groups, setFilter, this.syncConfig);
|
||||
}
|
||||
|
||||
return [groups, users];
|
||||
@@ -72,12 +68,15 @@ export class OktaDirectoryService extends BaseDirectoryService implements Direct
|
||||
const setFilter = this.createCustomSet(this.syncConfig.userFilter);
|
||||
|
||||
this.logService.info('Querying users.');
|
||||
const usersPromise = this.client.listUsers({ filter: oktaFilter }).each((user: any) => {
|
||||
const entry = this.buildUser(user);
|
||||
if (entry != null && !this.filterOutResult(setFilter, entry.email)) {
|
||||
entries.push(entry);
|
||||
}
|
||||
});
|
||||
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;
|
||||
@@ -86,12 +85,15 @@ export class OktaDirectoryService extends BaseDirectoryService implements Direct
|
||||
if (oktaFilter != null) {
|
||||
deactOktaFilter = '(' + oktaFilter + ') and ' + deactOktaFilter;
|
||||
}
|
||||
deactUsersPromise = this.client.listUsers({ filter: deactOktaFilter }).each((user: any) => {
|
||||
const entry = this.buildUser(user);
|
||||
if (entry != null && !this.filterOutResult(setFilter, entry.email)) {
|
||||
entries.push(entry);
|
||||
}
|
||||
});
|
||||
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();
|
||||
}
|
||||
@@ -104,7 +106,7 @@ export class OktaDirectoryService extends BaseDirectoryService implements Direct
|
||||
const entry = new UserEntry();
|
||||
entry.externalId = user.id;
|
||||
entry.referenceId = user.id;
|
||||
entry.email = user.profile.email;
|
||||
entry.email = user.profile.email != null ? user.profile.email.trim().toLowerCase() : null;
|
||||
entry.deleted = user.status === 'DEPROVISIONED';
|
||||
entry.disabled = user.status === 'SUSPENDED';
|
||||
return entry;
|
||||
@@ -116,10 +118,12 @@ export class OktaDirectoryService extends BaseDirectoryService implements Direct
|
||||
const oktaFilter = this.buildOktaFilter(this.syncConfig.groupFilter, force, lastSync);
|
||||
|
||||
this.logService.info('Querying groups.');
|
||||
await this.client.listGroups({ filter: oktaFilter }).each(async (group: any) => {
|
||||
const entry = await this.buildGroup(group);
|
||||
if (entry != null && !this.filterOutResult(setFilter, entry.name)) {
|
||||
entries.push(entry);
|
||||
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;
|
||||
@@ -131,8 +135,18 @@ export class OktaDirectoryService extends BaseDirectoryService implements Direct
|
||||
entry.referenceId = group.id;
|
||||
entry.name = group.profile.name;
|
||||
|
||||
await this.client.listGroupUsers(group.id).each((user: any) => {
|
||||
entry.userMemberExternalIds.add(user.id);
|
||||
// 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;
|
||||
@@ -152,4 +166,88 @@ export class OktaDirectoryService extends BaseDirectoryService implements Direct
|
||||
|
||||
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]);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
195
src/services/onelogin-directory.service.ts
Normal file
195
src/services/onelogin-directory.service.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
import { DirectoryType } from '../enums/directoryType';
|
||||
|
||||
import { GroupEntry } from '../models/groupEntry';
|
||||
import { OneLoginConfiguration } from '../models/oneLoginConfiguration';
|
||||
import { SyncConfiguration } from '../models/syncConfiguration';
|
||||
import { UserEntry } from '../models/userEntry';
|
||||
|
||||
import { BaseDirectoryService } from './baseDirectory.service';
|
||||
import { ConfigurationService } from './configuration.service';
|
||||
import { IDirectoryService } from './directory.service';
|
||||
|
||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
||||
import { LogService } from 'jslib-common/abstractions/log.service';
|
||||
|
||||
// Basic email validation: something@something.something
|
||||
const ValidEmailRegex = /^\S+@\S+\.\S+$/;
|
||||
|
||||
export class OneLoginDirectoryService extends BaseDirectoryService implements IDirectoryService {
|
||||
private dirConfig: OneLoginConfiguration;
|
||||
private syncConfig: SyncConfiguration;
|
||||
private accessToken: string;
|
||||
private allUsers: any[] = [];
|
||||
|
||||
constructor(private configurationService: ConfigurationService, private logService: LogService,
|
||||
private i18nService: I18nService) {
|
||||
super();
|
||||
}
|
||||
|
||||
async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> {
|
||||
const type = await this.configurationService.getDirectoryType();
|
||||
if (type !== DirectoryType.OneLogin) {
|
||||
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[]> {
|
||||
const entries: UserEntry[] = [];
|
||||
const query = this.createDirectoryQuery(this.syncConfig.userFilter);
|
||||
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) {
|
||||
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 entry;
|
||||
}
|
||||
|
||||
private async getAccessToken() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -4,35 +4,32 @@ import { GroupEntry } from '../models/groupEntry';
|
||||
import { SyncConfiguration } from '../models/syncConfiguration';
|
||||
import { UserEntry } from '../models/userEntry';
|
||||
|
||||
import { ImportDirectoryRequest } from 'jslib/models/request/importDirectoryRequest';
|
||||
import { ImportDirectoryRequestGroup } from 'jslib/models/request/importDirectoryRequestGroup';
|
||||
import { ImportDirectoryRequestUser } from 'jslib/models/request/importDirectoryRequestUser';
|
||||
import { OrganizationImportRequest } from 'jslib-common/models/request/organizationImportRequest';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { CryptoFunctionService } from 'jslib/abstractions/cryptoFunction.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { LogService } from 'jslib/abstractions/log.service';
|
||||
import { MessagingService } from 'jslib/abstractions/messaging.service';
|
||||
import { StorageService } from 'jslib/abstractions/storage.service';
|
||||
import { ApiService } from 'jslib-common/abstractions/api.service';
|
||||
import { CryptoFunctionService } from 'jslib-common/abstractions/cryptoFunction.service';
|
||||
import { EnvironmentService } from 'jslib-common/abstractions/environment.service';
|
||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
||||
import { LogService } from 'jslib-common/abstractions/log.service';
|
||||
import { MessagingService } from 'jslib-common/abstractions/messaging.service';
|
||||
|
||||
import { Utils } from 'jslib/misc/utils';
|
||||
import { Utils } from 'jslib-common/misc/utils';
|
||||
|
||||
import { AzureDirectoryService } from './azure-directory.service';
|
||||
import { ConfigurationService } from './configuration.service';
|
||||
import { DirectoryService } from './directory.service';
|
||||
import { IDirectoryService } from './directory.service';
|
||||
import { GSuiteDirectoryService } from './gsuite-directory.service';
|
||||
import { LdapDirectoryService } from './ldap-directory.service';
|
||||
import { OktaDirectoryService } from './okta-directory.service';
|
||||
|
||||
const Keys = {
|
||||
};
|
||||
import { OneLoginDirectoryService } from './onelogin-directory.service';
|
||||
|
||||
export class SyncService {
|
||||
private dirType: DirectoryType;
|
||||
|
||||
constructor(private configurationService: ConfigurationService, private logService: LogService,
|
||||
private cryptoFunctionService: CryptoFunctionService, private apiService: ApiService,
|
||||
private messagingService: MessagingService, private i18nService: I18nService) { }
|
||||
private messagingService: MessagingService, private i18nService: I18nService,
|
||||
private environmentService: EnvironmentService) { }
|
||||
|
||||
async sync(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> {
|
||||
this.dirType = await this.configurationService.getDirectoryType();
|
||||
@@ -52,15 +49,18 @@ export class SyncService {
|
||||
|
||||
this.messagingService.send('dirSyncStarted');
|
||||
try {
|
||||
const entries = await directoryService.getEntries(force, test);
|
||||
const entries = await directoryService.getEntries(force || syncConfig.overwriteExisting, test);
|
||||
let groups = entries[0];
|
||||
let users = entries[1];
|
||||
let users = this.filterUnsupportedUsers(entries[1]);
|
||||
|
||||
if (groups != null && groups.length > 0) {
|
||||
this.flattenUsersToGroups(groups, groups);
|
||||
}
|
||||
|
||||
if (test || ((groups == null || groups.length === 0) && (users == null || users.length === 0))) {
|
||||
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);
|
||||
}
|
||||
@@ -69,23 +69,29 @@ export class SyncService {
|
||||
return [groups, users];
|
||||
}
|
||||
|
||||
const req = this.buildRequest(groups, users, syncConfig.removeDisabled);
|
||||
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 hashBuf = await this.cryptoFunctionService.hash(this.apiService.baseUrl + reqJson, 'sha256');
|
||||
if (hashBuf != null) {
|
||||
hash = Utils.fromBufferToB64(hashBuf);
|
||||
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) {
|
||||
const orgId = await this.configurationService.getOrganizationId();
|
||||
if (orgId == null) {
|
||||
throw new Error('Organization not set.');
|
||||
}
|
||||
|
||||
await this.apiService.postImportDirectory(orgId, req);
|
||||
if (lastHash == null || (hash !== lastHash && hashLegacy !== lastHash)) {
|
||||
await this.apiService.postPublicImportDirectory(req);
|
||||
await this.configurationService.saveLastSyncHash(hash);
|
||||
} else {
|
||||
groups = null;
|
||||
@@ -106,18 +112,53 @@ export class SyncService {
|
||||
}
|
||||
}
|
||||
|
||||
private removeDuplicateUsers(users: UserEntry[]) {
|
||||
const uniqueUsers = new Array<UserEntry>();
|
||||
const processedUsers = 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
|
||||
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[] {
|
||||
return users == null ? null : users.filter(u => u.email?.length <= 256);
|
||||
}
|
||||
|
||||
private flattenUsersToGroups(levelGroups: GroupEntry[], allGroups: GroupEntry[]): Set<string> {
|
||||
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 childGroups = allGroups.filter(g => group.groupMemberReferenceIds.has(g.referenceId));
|
||||
const childUsers = this.flattenUsersToGroups(childGroups, allGroups);
|
||||
childUsers.forEach((id) => group.userMemberExternalIds.add(id));
|
||||
childUsers.forEach(id => group.userMemberExternalIds.add(id));
|
||||
allUsers = new Set([...allUsers, ...group.userMemberExternalIds]);
|
||||
}
|
||||
return allUsers;
|
||||
}
|
||||
|
||||
private getDirectoryService(): DirectoryService {
|
||||
private getDirectoryService(): IDirectoryService {
|
||||
switch (this.dirType) {
|
||||
case DirectoryType.GSuite:
|
||||
return new GSuiteDirectoryService(this.configurationService, this.logService, this.i18nService);
|
||||
@@ -127,35 +168,33 @@ export class SyncService {
|
||||
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): ImportDirectoryRequest {
|
||||
const model = new ImportDirectoryRequest();
|
||||
|
||||
if (groups != null) {
|
||||
for (const g of groups) {
|
||||
const ig = new ImportDirectoryRequestGroup();
|
||||
ig.name = g.name;
|
||||
ig.externalId = g.externalId;
|
||||
ig.users = Array.from(g.userMemberExternalIds);
|
||||
model.groups.push(ig);
|
||||
}
|
||||
}
|
||||
|
||||
if (users != null) {
|
||||
for (const u of users) {
|
||||
const iu = new ImportDirectoryRequestUser();
|
||||
iu.email = u.email;
|
||||
iu.externalId = u.externalId;
|
||||
iu.deleted = u.deleted || (removeDisabled && u.disabled);
|
||||
model.users.push(iu);
|
||||
}
|
||||
}
|
||||
|
||||
return model;
|
||||
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) {
|
||||
|
||||
100
src/utils.ts
Normal file
100
src/utils.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
||||
|
||||
import { SyncService } from './services/sync.service';
|
||||
|
||||
import { Entry } from './models/entry';
|
||||
import { LdapConfiguration } from './models/ldapConfiguration';
|
||||
import { SimResult } from './models/simResult';
|
||||
import { SyncConfiguration } from './models/syncConfiguration';
|
||||
import { UserEntry } from './models/userEntry';
|
||||
|
||||
export class ConnectorUtils {
|
||||
static async simulate(syncService: SyncService, i18nService: I18nService, sinceLast: boolean): Promise<SimResult> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const simResult = new SimResult();
|
||||
try {
|
||||
const result = await syncService.sync(!sinceLast, true);
|
||||
if (result[0] != null) {
|
||||
simResult.groups = result[0];
|
||||
}
|
||||
if (result[1] != null) {
|
||||
simResult.users = result[1];
|
||||
}
|
||||
} catch (e) {
|
||||
simResult.groups = null;
|
||||
simResult.users = null;
|
||||
reject(e || i18nService.t('syncError'));
|
||||
return;
|
||||
}
|
||||
|
||||
const userMap = new Map<string, UserEntry>();
|
||||
this.sortEntries(simResult.users, i18nService);
|
||||
for (const u of simResult.users) {
|
||||
userMap.set(u.externalId, u);
|
||||
if (u.deleted) {
|
||||
simResult.deletedUsers.push(u);
|
||||
} else if (u.disabled) {
|
||||
simResult.disabledUsers.push(u);
|
||||
} else {
|
||||
simResult.enabledUsers.push(u);
|
||||
}
|
||||
}
|
||||
|
||||
this.sortEntries(simResult.groups, i18nService);
|
||||
for (const g of simResult.groups) {
|
||||
if (g.userMemberExternalIds == null) {
|
||||
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) {
|
||||
if (sync.interval <= 0) {
|
||||
sync.interval = null;
|
||||
} 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -8,37 +8,32 @@
|
||||
"target": "ES2016",
|
||||
"allowJs": true,
|
||||
"sourceMap": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"types": [],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"jslib/*": [
|
||||
"jslib/src/*"
|
||||
"tldjs": [
|
||||
"jslib/src/misc/tldjs.noop"
|
||||
],
|
||||
"@angular/*": [
|
||||
"node_modules/@angular/*"
|
||||
"jslib-common/*": [
|
||||
"jslib/common/src/*"
|
||||
],
|
||||
"angular2-toaster": [
|
||||
"node_modules/angular2-toaster"
|
||||
"jslib-angular/*": [
|
||||
"jslib/angular/src/*"
|
||||
],
|
||||
"angulartics2": [
|
||||
"node_modules/angulartics2"
|
||||
"jslib-electron/*": [
|
||||
"jslib/electron/src/*"
|
||||
],
|
||||
"electron": [
|
||||
"node_modules/electron"
|
||||
"jslib-node/*": [
|
||||
"jslib/node/src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"jslib/node_modules",
|
||||
"jslib/src/services/webCryptoFunction.service.ts",
|
||||
"jslib/src/services/search.service.ts",
|
||||
"jslib/src/services/nodeApi.service.ts",
|
||||
"jslib/src/services/export.service.ts",
|
||||
"jslib/src/angular/components/export.component.ts",
|
||||
"dist",
|
||||
"jslib/dist",
|
||||
"build",
|
||||
"jslib/spec"
|
||||
"angularCompilerOptions": {
|
||||
"preserveWhitespaces": true
|
||||
},
|
||||
"include": [
|
||||
"src",
|
||||
"src-cli"
|
||||
]
|
||||
}
|
||||
|
||||
22
tslint.json
22
tslint.json
@@ -48,6 +48,28 @@
|
||||
"check-preblock",
|
||||
"check-separator",
|
||||
"check-type"
|
||||
],
|
||||
"max-classes-per-file": false,
|
||||
"ordered-imports": true,
|
||||
"arrow-parens": [
|
||||
true,
|
||||
"ban-single-arg-parens"
|
||||
],
|
||||
"semicolon": [
|
||||
true,
|
||||
"always"
|
||||
],
|
||||
"trailing-comma": [
|
||||
true,
|
||||
{
|
||||
"multiline": {
|
||||
"objects": "always",
|
||||
"arrays": "always",
|
||||
"functions": "ignore",
|
||||
"typeLiterals": "ignore"
|
||||
},
|
||||
"singleline": "never"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
76
webpack.cli.js
Normal file
76
webpack.cli.js
Normal file
@@ -0,0 +1,76 @@
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||
const nodeExternals = require('webpack-node-externals');
|
||||
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
|
||||
|
||||
if (process.env.NODE_ENV == null) {
|
||||
process.env.NODE_ENV = 'development';
|
||||
}
|
||||
const ENV = process.env.ENV = process.env.NODE_ENV;
|
||||
|
||||
const moduleRules = [
|
||||
{
|
||||
test: /\.ts$/,
|
||||
enforce: 'pre',
|
||||
loader: 'tslint-loader',
|
||||
},
|
||||
{
|
||||
test: /\.ts$/,
|
||||
loaders: ['ts-loader'],
|
||||
exclude: path.resolve(__dirname, 'node_modules'),
|
||||
},
|
||||
{
|
||||
test: /\.node$/,
|
||||
loader: 'node-loader',
|
||||
},
|
||||
];
|
||||
|
||||
const plugins = [
|
||||
new CleanWebpackPlugin(),
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [
|
||||
{ from: './src/locales', to: 'locales' },
|
||||
],
|
||||
}),
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.BWCLI_ENV': JSON.stringify(ENV),
|
||||
}),
|
||||
new webpack.BannerPlugin({
|
||||
banner: '#!/usr/bin/env node',
|
||||
raw: true
|
||||
}),
|
||||
new webpack.IgnorePlugin(/^encoding$/, /node-fetch/),
|
||||
];
|
||||
|
||||
const config = {
|
||||
mode: ENV,
|
||||
target: 'node',
|
||||
devtool: ENV === 'development' ? 'eval-source-map' : 'source-map',
|
||||
node: {
|
||||
__dirname: false,
|
||||
__filename: false,
|
||||
},
|
||||
entry: {
|
||||
'bwdc': './src/bwdc.ts',
|
||||
},
|
||||
optimization: {
|
||||
minimize: false,
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.ts', '.js', '.json'],
|
||||
plugins: [new TsconfigPathsPlugin({ configFile: './tsconfig.json' })],
|
||||
symlinks: false,
|
||||
modules: [path.resolve('node_modules')],
|
||||
},
|
||||
output: {
|
||||
filename: '[name].js',
|
||||
path: path.resolve(__dirname, 'build-cli'),
|
||||
},
|
||||
module: { rules: moduleRules },
|
||||
plugins: plugins,
|
||||
externals: [nodeExternals()],
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
@@ -1,9 +1,9 @@
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
const merge = require('webpack-merge');
|
||||
const { merge } = require('webpack-merge');
|
||||
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 TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
|
||||
|
||||
const common = {
|
||||
module: {
|
||||
@@ -11,56 +11,58 @@ const common = {
|
||||
{
|
||||
test: /\.ts$/,
|
||||
enforce: 'pre',
|
||||
loader: 'tslint-loader'
|
||||
loader: 'tslint-loader',
|
||||
},
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
use: 'ts-loader',
|
||||
exclude: /node_modules\/(?!(@bitwarden)\/).*/
|
||||
exclude: /node_modules\/(?!(@bitwarden)\/).*/,
|
||||
},
|
||||
]
|
||||
],
|
||||
},
|
||||
plugins: [],
|
||||
resolve: {
|
||||
extensions: ['.tsx', '.ts', '.js'],
|
||||
alias: {
|
||||
jslib: path.join(__dirname, 'jslib/src')
|
||||
}
|
||||
plugins: [new TsconfigPathsPlugin({ configFile: './tsconfig.json' })],
|
||||
},
|
||||
output: {
|
||||
filename: '[name].js',
|
||||
path: path.resolve(__dirname, 'build')
|
||||
}
|
||||
path: path.resolve(__dirname, 'build'),
|
||||
},
|
||||
};
|
||||
|
||||
const main = {
|
||||
mode: 'production',
|
||||
target: 'electron-main',
|
||||
node: {
|
||||
__dirname: false,
|
||||
__filename: false
|
||||
__filename: false,
|
||||
},
|
||||
entry: {
|
||||
'main': './src/main.ts'
|
||||
'main': './src/main.ts',
|
||||
},
|
||||
optimization: {
|
||||
minimize: false,
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.node$/,
|
||||
loader: 'node-loader'
|
||||
loader: 'node-loader',
|
||||
},
|
||||
]
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new CleanWebpackPlugin([
|
||||
path.resolve(__dirname, 'build/*')
|
||||
]),
|
||||
new CopyWebpackPlugin([
|
||||
'./src/package.json',
|
||||
{ from: './src/images', to: 'images' },
|
||||
{ from: './src/locales', to: 'locales' },
|
||||
]),
|
||||
new CleanWebpackPlugin(),
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [
|
||||
'./src/package.json',
|
||||
{ from: './src/images', to: 'images' },
|
||||
{ from: './src/locales', to: 'locales' },
|
||||
],
|
||||
}),
|
||||
],
|
||||
externals: [nodeExternals()]
|
||||
externals: [nodeExternals()],
|
||||
};
|
||||
|
||||
module.exports = merge(common, main);
|
||||
|
||||
@@ -1,23 +1,10 @@
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
const merge = require('webpack-merge');
|
||||
const { merge } = require('webpack-merge');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const GoogleFontsPlugin = require("google-fonts-webpack-plugin");
|
||||
const ExtractTextPlugin = require('extract-text-webpack-plugin');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
const AngularCompilerPlugin = require('@ngtools/webpack').AngularCompilerPlugin;
|
||||
|
||||
const isVendorModule = (module) => {
|
||||
if (!module.context) {
|
||||
return false;
|
||||
}
|
||||
return module.context.indexOf('node_modules') !== -1;
|
||||
};
|
||||
|
||||
const extractCss = new ExtractTextPlugin({
|
||||
filename: '[name].css',
|
||||
disable: false,
|
||||
allChunks: true
|
||||
});
|
||||
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
|
||||
|
||||
const common = {
|
||||
module: {
|
||||
@@ -25,11 +12,11 @@ const common = {
|
||||
{
|
||||
test: /\.ts$/,
|
||||
enforce: 'pre',
|
||||
loader: 'tslint-loader'
|
||||
loader: 'tslint-loader',
|
||||
},
|
||||
{
|
||||
test: /(?:\.ngfactory\.js|\.ngstyle\.js|\.ts)$/,
|
||||
loader: '@ngtools/webpack'
|
||||
loader: '@ngtools/webpack',
|
||||
},
|
||||
{
|
||||
test: /\.(jpe?g|png|gif|svg)$/i,
|
||||
@@ -39,39 +26,53 @@ const common = {
|
||||
options: {
|
||||
name: '[name].[ext]',
|
||||
outputPath: 'images/',
|
||||
}
|
||||
}]
|
||||
}
|
||||
]
|
||||
},
|
||||
}],
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [],
|
||||
resolve: {
|
||||
extensions: ['.tsx', '.ts', '.js', '.json'],
|
||||
alias: {
|
||||
jslib: path.join(__dirname, 'jslib/src')
|
||||
},
|
||||
plugins: [new TsconfigPathsPlugin({ configFile: './tsconfig.json' })],
|
||||
symlinks: false,
|
||||
modules: [path.resolve('node_modules')]
|
||||
modules: [path.resolve('node_modules')],
|
||||
},
|
||||
output: {
|
||||
filename: '[name].js',
|
||||
path: path.resolve(__dirname, 'build')
|
||||
}
|
||||
path: path.resolve(__dirname, 'build'),
|
||||
},
|
||||
};
|
||||
|
||||
const renderer = {
|
||||
mode: 'production',
|
||||
devtool: false,
|
||||
target: 'electron-renderer',
|
||||
node: {
|
||||
__dirname: false
|
||||
__dirname: false,
|
||||
},
|
||||
entry: {
|
||||
'app/main': './src/app/main.ts'
|
||||
'app/main': './src/app/main.ts',
|
||||
},
|
||||
optimization: {
|
||||
minimize: false,
|
||||
splitChunks: {
|
||||
cacheGroups: {
|
||||
commons: {
|
||||
test: /[\\/]node_modules[\\/]/,
|
||||
name: 'app/vendor',
|
||||
chunks: (chunk) => {
|
||||
return chunk.name === 'app/main';
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(html)$/,
|
||||
loader: 'html-loader'
|
||||
loader: 'html-loader',
|
||||
},
|
||||
{
|
||||
test: /.(ttf|otf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/,
|
||||
@@ -80,65 +81,53 @@ const renderer = {
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: '[name].[ext]',
|
||||
outputPath: 'fonts/'
|
||||
}
|
||||
}]
|
||||
outputPath: 'fonts/',
|
||||
},
|
||||
}],
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
use: extractCss.extract({
|
||||
use: [
|
||||
{
|
||||
loader: 'css-loader',
|
||||
use: [
|
||||
{
|
||||
loader: MiniCssExtractPlugin.loader,
|
||||
options: {
|
||||
publicPath: '../',
|
||||
},
|
||||
{
|
||||
loader: 'sass-loader',
|
||||
}
|
||||
],
|
||||
publicPath: '../'
|
||||
})
|
||||
},
|
||||
'css-loader',
|
||||
'sass-loader',
|
||||
],
|
||||
},
|
||||
]
|
||||
// Hide System.import warnings. ref: https://github.com/angular/angular/issues/21560
|
||||
{
|
||||
test: /[\/\\]@angular[\/\\].+\.js$/,
|
||||
parser: { system: true },
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new GoogleFontsPlugin({
|
||||
fonts: [
|
||||
{
|
||||
family: 'Open Sans',
|
||||
variants: ['300', '300italic', '400', '400italic', '600', '600italic',
|
||||
'700', '700italic', '800', '800italic'],
|
||||
subsets: ['cyrillic', 'cyrillic-ext', 'greek', 'greek-ext', 'latin', 'latin-ext']
|
||||
}
|
||||
],
|
||||
formats: ['woff2'],
|
||||
path: 'fonts/',
|
||||
filename: 'css/fonts.css'
|
||||
}),
|
||||
new AngularCompilerPlugin({
|
||||
tsConfigPath: 'tsconfig.json',
|
||||
entryModule: 'src/app/app.module#AppModule',
|
||||
sourceMap: true
|
||||
sourceMap: true,
|
||||
}),
|
||||
// ref: https://github.com/angular/angular/issues/20357
|
||||
new webpack.ContextReplacementPlugin(/\@angular(\\|\/)core(\\|\/)esm5/,
|
||||
new webpack.ContextReplacementPlugin(/\@angular(\\|\/)core(\\|\/)fesm5/,
|
||||
path.resolve(__dirname, './src')),
|
||||
new webpack.optimize.CommonsChunkPlugin({
|
||||
name: 'app/vendor',
|
||||
chunks: ['app/main'],
|
||||
minChunks: isVendorModule
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: './src/index.html',
|
||||
filename: 'index.html',
|
||||
chunks: ['app/vendor', 'app/main']
|
||||
}),
|
||||
new webpack.SourceMapDevToolPlugin({
|
||||
filename: '[name].js.map',
|
||||
include: ['app/main.js']
|
||||
}),
|
||||
new webpack.DefinePlugin({ 'global.GENTLY': false }),
|
||||
extractCss
|
||||
]
|
||||
new MiniCssExtractPlugin({
|
||||
filename: '[name].[hash].css',
|
||||
chunkFilename: '[id].[hash].css',
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
module.exports = merge(common, renderer);
|
||||
|
||||
Reference in New Issue
Block a user