1
0
mirror of https://github.com/bitwarden/directory-connector synced 2025-12-14 15:23:16 +00:00

Compare commits

..

169 Commits

Author SHA1 Message Date
Kyle Spearrin
3335659c8d bump version 2019-03-26 12:24:16 -04:00
Kyle Spearrin
f0282b33f0 add paging to members list api 2019-03-26 09:06:42 -04:00
Kyle Spearrin
f69d461463 move scope of filters for delUsers query 2019-03-26 09:02:35 -04:00
Elie Mélois
bcf16562fe Manage multiple pages when fetching users (#15) 2019-03-26 08:57:01 -04:00
Kyle Spearrin
d7412410f2 group paging cleanup 2019-03-26 08:02:09 -04:00
Elie Mélois
d5907477df Manage multiple pages when fetching groups (#14) 2019-03-26 07:58:05 -04:00
Kyle Spearrin
9acfaff361 remove echo 2019-03-22 22:25:37 -04:00
Kyle Spearrin
737274da59 test for ver info during install 2019-03-22 22:20:41 -04:00
Kyle Spearrin
7a0e0e6915 WIN_PKG set with ps 2019-03-22 22:16:00 -04:00
Kyle Spearrin
25847b9c83 ver info testing 2019-03-22 22:11:32 -04:00
Kyle Spearrin
07dd7bb93e bump version 2019-03-22 17:35:29 -04:00
Kyle Spearrin
bc9936bd92 update lock file 2019-03-22 17:29:38 -04:00
Kyle Spearrin
e4428e3387 update ldapjs fork 2019-03-22 17:21:18 -04:00
Kyle Spearrin
30ef0ce140 back to node 10 2019-03-22 16:55:51 -04:00
Kyle Spearrin
38c461ebe0 use ldapjs fork 2019-03-22 16:55:20 -04:00
Kyle Spearrin
ead1776f90 try node 9 2019-03-22 16:38:49 -04:00
Kyle Spearrin
0e944b2442 simplify sim promise 2019-03-22 13:12:52 -04:00
Kyle Spearrin
3bbd503308 RELEASE_NAME is just numbers 2019-03-20 23:09:54 -04:00
Kyle Spearrin
a7b13b168e release name fix 2019-03-20 23:09:12 -04:00
Kyle Spearrin
1cf8ae1bdf bump version 2019-03-20 22:16:31 -04:00
Kyle Spearrin
7da3c8e252 Merge branch 'master' of github.com:bitwarden/directory-connector 2019-03-20 17:33:13 -04:00
Kyle Spearrin
2edf3fb68d data-file and last-sync commands 2019-03-20 17:33:11 -04:00
Kyle Spearrin
1e698ba81b Update README.md 2019-03-20 16:44:41 -04:00
Kyle Spearrin
e9034ea6fe Update README.md 2019-03-20 16:43:48 -04:00
Kyle Spearrin
08ebc1ce16 build for dist 2019-03-20 14:54:52 -04:00
Kyle Spearrin
a80c95e1cf version test is only windows 2019-03-20 12:26:09 -04:00
Kyle Spearrin
c3ab785d66 test version script 2019-03-20 12:09:23 -04:00
Kyle Spearrin
baef6210c1 update keytar 2019-03-20 11:45:50 -04:00
Kyle Spearrin
74d4a6fa72 update jslib 2019-03-19 15:53:23 -04:00
Kyle Spearrin
770e41a657 bump version 2019-03-19 09:07:51 -04:00
Kyle Spearrin
d04eb03b3c node types to v10 2019-03-18 14:55:09 -04:00
Kyle Spearrin
9a8224a8d6 config different settings 2019-03-18 14:18:53 -04:00
Kyle Spearrin
bd072b727c clear cache command 2019-03-18 13:04:23 -04:00
Kyle Spearrin
93cedec32c BWCLI_DEBUG for logging 2019-03-18 10:40:47 -04:00
Kyle Spearrin
3d8933e5f0 login and logout commands 2019-03-18 10:34:11 -04:00
Kyle Spearrin
790f5a5f2f properly extract and untar keytar.node 2019-03-18 09:10:13 -04:00
Kyle Spearrin
36bb24d580 enable linux builds 2019-03-16 23:47:05 -04:00
Kyle Spearrin
3e08ba95cf fixes 2019-03-16 23:45:58 -04:00
Kyle Spearrin
e50f82175b include keytar.node prebuilds 2019-03-16 23:30:53 -04:00
Kyle Spearrin
5907c8b5ce macos artifact fix 2019-03-16 22:25:47 -04:00
Kyle Spearrin
9f9b5beff9 fix deploy regex 2019-03-16 22:17:28 -04:00
Kyle Spearrin
71dbd31477 dist-cli paths 2019-03-16 22:15:19 -04:00
Kyle Spearrin
9e4d17cc7c fix PACKAGE_VERSION env variable 2019-03-16 22:07:58 -04:00
Kyle Spearrin
2ae14913cc install checksum 2019-03-16 22:02:38 -04:00
Kyle Spearrin
a4e23eee83 use cmd and sh 2019-03-16 21:53:57 -04:00
Kyle Spearrin
0bc406b332 appveyor.yml build 2019-03-16 21:42:11 -04:00
Kyle Spearrin
76a6b2407e remove pkg and keytar check 2019-03-16 20:57:17 -04:00
Kyle Spearrin
f371067d56 debug 2019-03-16 20:43:38 -04:00
Kyle Spearrin
d1507dffca check for keytar.node in same dir when packaged 2019-03-16 16:44:33 -04:00
Kyle Spearrin
c11cc0b979 pkg directory connector cli 2019-03-16 11:58:27 -04:00
Kyle Spearrin
7cc941cc84 base cli program and sync command 2019-03-16 11:27:16 -04:00
Kyle Spearrin
a847339d72 filter info logs on CLI 2019-03-16 10:47:25 -04:00
Kyle Spearrin
46827cdaa0 test command for cli 2019-03-16 00:45:17 -04:00
Kyle Spearrin
183b54f9b8 stringify and parse json 2019-03-16 00:38:41 -04:00
Kyle Spearrin
e51a70f7b9 added missing runs 2019-03-15 23:09:02 -04:00
Kyle Spearrin
f83e97abfc rebuild and reset 2019-03-15 23:06:37 -04:00
Kyle Spearrin
92a566e73e fix repo url 2019-03-15 22:39:05 -04:00
Kyle Spearrin
59ade0d2ba stub out cli for directory connector 2019-03-15 22:38:02 -04:00
Kyle Spearrin
7da368d555 remove user deltas from azure ad 2019-03-15 12:01:11 -04:00
Kyle Spearrin
e00fd648a6 move to electron-store 2019-03-11 22:45:27 -04:00
Kyle Spearrin
f74c8420fb update jslib 2019-03-06 08:45:36 -05:00
Kyle Spearrin
d2fbb8aba9 update jslib 2019-03-02 15:08:46 -05:00
Kyle Spearrin
ba8aee2148 format html files 2019-02-21 16:54:24 -05:00
Kyle Spearrin
018dfd8865 null checks 2019-02-14 15:57:52 -05:00
Kyle Spearrin
be40ee3100 update to angular 7 2019-01-25 15:23:09 -05:00
Kyle Spearrin
35f9b5ed17 update jslib 2019-01-20 23:27:28 -05:00
Kyle Spearrin
8c4450246b update jslib 2019-01-03 10:24:47 -05:00
Kyle Spearrin
537e7d10e6 update jslib 2018-12-31 13:09:08 -05:00
Kyle Spearrin
124c7288b9 zxcvbn types 2018-12-26 15:14:46 -05:00
Kyle Spearrin
3224648bc7 update electron 2018-12-26 15:11:28 -05:00
Kyle Spearrin
9b8a11b65e ignore index files 2018-12-18 22:23:56 -05:00
Kyle Spearrin
6b9d72f4a2 ignore password generator components 2018-12-18 22:10:50 -05:00
Kyle Spearrin
8d50697345 exclude some stuff 2018-12-18 17:31:09 -05:00
Kyle Spearrin
f8e9d2b73a duo_web_sdk fix 2018-12-18 17:19:52 -05:00
Kyle Spearrin
99ed78a2e6 install and use duo_web_sdk w/ npm 2018-12-18 17:00:43 -05:00
Kyle Spearrin
346dab2c26 update jslib 2018-12-13 11:30:24 -05:00
Kyle Spearrin
28582b2b0c update gulp to 4.0.0 2018-11-27 12:44:23 -05:00
Kyle Spearrin
2bda7e5f65 pass project name to updater 2018-11-27 08:32:40 -05:00
Kyle Spearrin
8849f6c513 ignore passwordGeneration.service.ts abstraction 2018-11-24 09:12:50 -05:00
Kyle Spearrin
6d50856c07 ignore password generation service 2018-11-23 16:08:35 -05:00
Kyle Spearrin
bdec0f9493 update jslib 2018-11-23 09:45:19 -05:00
Kyle Spearrin
ab31beb1ae update jslib 2018-11-15 15:45:17 -05:00
Kyle Spearrin
014d995284 update deps 2018-11-09 22:38:27 -05:00
Kyle Spearrin
69783d3606 update jslib 2018-11-09 22:37:36 -05:00
Kyle Spearrin
55a0078332 fix lock file 2018-11-07 12:34:29 -05:00
Kyle Spearrin
346e0194d4 update jslib 2018-11-07 12:33:32 -05:00
Kyle Spearrin
512f222819 update jslib 2018-11-07 12:11:10 -05:00
Kyle Spearrin
006c6b3312 update jslib 2018-11-06 16:06:38 -05:00
Kyle Spearrin
8c80a52ce2 update jslib 2018-11-06 09:05:42 -05:00
Kyle Spearrin
5d8bf54f2f update jslib 2018-10-29 13:55:45 -04:00
Kyle Spearrin
70c1eb0623 formatting 2018-10-26 07:34:16 -04:00
Jan Hajek
447b674469 Add support for filtering users based on their group membership (#9)
* Add support for filtering users based on their group membership.

* Fix async call in if statement and proper keyword detection.

* Handle case where the checkMemberGroups was failing due to deleted user.

* Pass UseEntry into the filter and simplify filter condition.

* Revert changes in package-lock.json
2018-10-26 07:29:58 -04:00
Kyle Spearrin
7cb2147569 update jslib 2018-10-16 08:57:14 -04:00
Kyle Spearrin
327de4d714 update jslib 2018-10-13 23:29:54 -04:00
Kyle Spearrin
9b57955d72 update jslib 2018-10-11 21:27:04 -04:00
Kyle Spearrin
e63198e130 update electron 2018-10-09 17:56:23 -04:00
Kyle Spearrin
03eac9006f update jslib 2018-10-09 15:32:23 -04:00
Kyle Spearrin
d3402cca5e update jslib 2018-10-05 13:57:36 -04:00
Kyle Spearrin
076b320e70 dont await void methods 2018-10-04 12:05:47 -04:00
Kyle Spearrin
44180f95a5 convert toaster and analytics to platform utils 2018-10-03 10:15:00 -04:00
Kyle Spearrin
e23ebf13b7 update jslib 2018-10-02 09:22:53 -04:00
Kyle Spearrin
e0fd1a2e93 fix null ref on trim, resolves #8 2018-09-20 08:17:21 -04:00
Kyle Spearrin
c39986b37c import webfonts 2018-09-18 15:49:48 -04:00
Kyle Spearrin
1e0029dc15 bump version 2018-09-18 11:52:32 -04:00
Kyle Spearrin
63bc5e4161 ssl options for ldap 2018-09-18 11:31:11 -04:00
Kyle Spearrin
8c8b4da595 no minimize 2018-09-14 15:48:50 -04:00
Kyle Spearrin
1a7536270a bump version 2018-09-14 12:19:16 -04:00
Kyle Spearrin
d92e3c8d7b preserveWhitespaces 2018-09-13 12:04:13 -04:00
Kyle Spearrin
c30aedf491 update jslib 2018-09-12 13:22:54 -04:00
Kyle Spearrin
0e96a462ee update libs and move to webpack 4 2018-09-12 10:17:06 -04:00
Kyle Spearrin
3ef60cb3a0 update jslib 2018-09-11 08:45:46 -04:00
Kyle Spearrin
34b7638e11 trim email also 2018-09-08 08:13:40 -04:00
Kyle Spearrin
63ef932469 dont use group deltas since they do not update on membership changes 2018-09-07 08:41:55 -04:00
Kyle Spearrin
e09163fc72 update lunr 2018-09-03 21:51:37 -04:00
Kyle Spearrin
6a737f6d7d Fix !== null checks 2018-08-30 21:48:17 -04:00
Kyle Spearrin
ccc7b7b213 update jslib 2018-08-29 09:22:37 -04:00
Kyle Spearrin
1e98bc6430 fall back to userPrincipalName in other conditions 2018-08-29 08:49:59 -04:00
Kyle Spearrin
c63a945057 update jslib 2018-08-28 23:17:53 -04:00
Kyle Spearrin
a13d9fa00f add csp 2018-08-28 12:15:12 -04:00
Kyle Spearrin
1518d5eafc update jslib 2018-08-27 23:05:39 -04:00
Kyle Spearrin
c74800fd8c update bootstrap 2018-08-21 15:28:12 -04:00
Kyle Spearrin
9e282efe28 npm audit fix 2018-08-21 15:18:55 -04:00
Kyle Spearrin
26efc9158d ignore notifications service 2018-08-20 22:22:48 -04:00
Kyle Spearrin
e2d3d35d71 update jslib 2018-08-20 17:08:27 -04:00
Kyle Spearrin
c115bf84f0 setUrlsFromStorage on init factory 2018-08-20 17:08:07 -04:00
Kyle Spearrin
5b56d32492 update electron 2018-08-18 21:44:10 -04:00
Kyle Spearrin
2fe294ad8a fix click issue on 2fa provider selection 2018-08-17 08:56:31 -04:00
Kyle Spearrin
1343897fa9 update jslib 2018-08-17 08:32:38 -04:00
Kyle Spearrin
9b2b0e4ea6 update jslib 2018-08-16 15:13:34 -04:00
Kyle Spearrin
489e775b08 update jslib 2018-08-15 09:01:30 -04:00
Kyle Spearrin
0d76a45181 bump version 2018-08-14 21:44:34 -04:00
Kyle Spearrin
4787f9a462 prelogin kdf info 2018-08-14 15:13:44 -04:00
Kyle Spearrin
b228e12c81 update jslib 2018-08-13 23:41:14 -04:00
Kyle Spearrin
0e28100ec6 add papaparse types cause whoknowswhy 2018-08-08 17:00:33 -04:00
Kyle Spearrin
d2c1697144 switch to gulp file to get google fonts 2018-08-08 12:53:09 -04:00
Kyle Spearrin
b924ea8dd1 update packages for node 10 2018-08-08 12:28:32 -04:00
Kyle Spearrin
30936d3ad1 update jslib 2018-08-07 09:26:14 -04:00
Kyle Spearrin
289589dce9 toaster message paragraph styles 2018-08-02 08:48:59 -04:00
Kyle Spearrin
48734b8109 update jslib 2018-07-31 23:49:46 -04:00
Kyle Spearrin
2e69a41943 update jslib 2018-07-31 11:26:33 -04:00
Kyle Spearrin
f414a3ac0a update jslib 2018-07-30 17:08:49 -04:00
Kyle Spearrin
81833b134c update jslib 2018-07-25 14:30:40 -04:00
Kyle Spearrin
5a5b74a0d5 bump version 2018-07-24 15:29:48 -04:00
Kyle Spearrin
5a7a230193 update libs 2018-07-24 15:29:00 -04:00
Jacob
bc5ab6252e Fetch image from bitwarden/brand (#7)
That was the last of them, sorry for all the pull request spams :P
2018-07-19 10:51:35 -04:00
Kyle Spearrin
0e4d5e0973 update jslib 2018-07-18 09:56:19 -04:00
Kyle Spearrin
08e254e34c version bump 2018-07-17 08:49:46 -04:00
Kyle Spearrin
2c20796ea1 make sure entry ids are unique 2018-07-17 08:49:15 -04:00
Kyle Spearrin
8b9d7a7e2e remove sync service ref 2018-07-13 10:51:14 -04:00
Kyle Spearrin
1a0709b02c update jslib 2018-07-13 09:31:32 -04:00
Kyle Spearrin
0f386ee8d2 remember email on login 2018-07-13 09:29:12 -04:00
Kyle Spearrin
c0b41155e9 update jslib 2018-07-09 09:14:46 -04:00
Kyle Spearrin
ddd1165728 dummy module update 2018-07-07 23:57:24 -04:00
Kyle Spearrin
651dbe59c8 update jslib 2018-07-07 23:52:19 -04:00
Kyle Spearrin
aeb6f28f9a update jslib 2018-07-05 14:55:06 -04:00
Kyle Spearrin
012eefad4e update jslib 2018-07-05 14:42:58 -04:00
Kyle Spearrin
b05346d4a3 update jslib 2018-06-30 13:51:32 -04:00
Kyle Spearrin
854e6e84fe ignore jslib importers 2018-06-28 08:07:02 -04:00
Kyle Spearrin
20436bf07d tolowercase all the emails 2018-06-28 07:53:58 -04:00
Kyle Spearrin
4110e4b8cb update jslib 2018-06-13 22:14:28 -04:00
Kyle Spearrin
e179edc2e0 update jslib 2018-06-13 17:15:21 -04:00
Kyle Spearrin
133ef7eb16 load DuoWebSDK as a module 2018-06-11 13:33:03 -04:00
Kyle Spearrin
5a98982fc2 move dummy module into project 2018-06-11 08:54:34 -04:00
Kyle Spearrin
f6e92bf597 bump version 2018-06-08 16:08:12 -04:00
Kyle Spearrin
af4a8d5ae8 update jslib 2018-06-04 12:06:04 -04:00
Kyle Spearrin
c3704bbb67 bump version 2018-06-01 16:42:01 -04:00
Kyle Spearrin
8620f2f80d update jslib 2018-06-01 12:34:11 -04:00
Kyle Spearrin
e0124474cf update jslib 2018-06-01 12:00:58 -04:00
Kyle Spearrin
ac21b78225 Update README.md 2018-06-01 09:32:08 -04:00
59 changed files with 10906 additions and 5562 deletions

3
.gitignore vendored
View File

@@ -4,8 +4,11 @@ node_modules
npm-debug.log npm-debug.log
vwd.webinfo vwd.webinfo
dist/ dist/
dist-cli/
css/
*.crx *.crx
*.pem *.pem
build-cli/
build/ build/
yarn-error.log yarn-error.log
.DS_Store .DS_Store

View File

@@ -14,11 +14,36 @@ Supported directories:
The application is written using Electron with Angular and installs on Windows, macOS, and Linux distributions. 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> [![Platforms](https://imgur.com/SLv9paA.png "Windows, macOS, and Linux")](https://help.bitwarden.com/article/directory-sync/#download-and-install)
![Directory Connector](http://imgur.com/I6FjN4j.png "Dashboard") ![Directory Connector](https://raw.githubusercontent.com/bitwarden/brand/master/screenshots/directory-connector-macos.png "Dashboard")
# 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** **Requirements**
@@ -29,10 +54,26 @@ The application is written using Electron with Angular and installs on Windows,
```bash ```bash
npm install npm install
npm run reset # Only necessary if you have previously run the CLI app
npm run rebuild
npm run electron 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. 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.

162
appveyor.yml Normal file
View File

@@ -0,0 +1,162 @@
image:
- Visual Studio 2017
- Ubuntu1804
branches:
except:
- l10n_master
environment:
WIN_PKG: C:\Users\appveyor\.pkg-cache\v2.5\fetched-v10.4.1-win-x64
stack: node 10
init:
- ps: |
if($isWindows -and $env:DEBUG_RDP -eq "true") {
iex ((new-object net.webclient).DownloadString(`
'https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1'))
}
- sh: sudo apt-get update
- sh: sudo apt-get -y install pkg-config libxss-dev libsecret-1-dev rpm
- ps: |
if($isWindows) {
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Install-Product node 10
$env:PATH = "C:\Program Files (x86)\Resource Hacker;${env:PATH}"
}
if($env:APPVEYOR_REPO_TAG -eq "true") {
$env:RELEASE_NAME = $env:APPVEYOR_REPO_TAG_NAME.TrimStart("v")
}
install:
- ps: |
$env:PACKAGE_VERSION = (Get-Content -Raw -Path .\src\package.json | ConvertFrom-Json).version
$env:PROD_DEPLOY = "false"
if($env:APPVEYOR_REPO_TAG -eq "true" -and $env:APPVEYOR_RE_BUILD -eq "True") {
$env:PROD_DEPLOY = "true"
echo "This is a production deployment."
}
if($isWindows) {
if(Test-Path -Path $env:WIN_PKG) {
$env:VER_INFO = "true"
}
choco install reshack --no-progress
choco install cloc --no-progress
choco install checksum --no-progress
cloc --include-lang TypeScript,JavaScript,HTML,Sass,CSS --vcs git
.\make-versioninfo.ps1
}
- ps: |
if($isWindows) {
$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"
}
before_build:
- node --version
- npm --version
build_script:
- cmd: |
if defined VER_INFO ResourceHacker -open %WIN_PKG% -save %WIN_PKG% -action delete -mask ICONGROUP,1,
if defined VER_INFO ResourceHacker -open version-info.rc -save version-info.res -action compile
if defined VER_INFO ResourceHacker -open %WIN_PKG% -save %WIN_PKG% -action addoverwrite -resource version-info.res
- sh: npm install
- sh: npm run rebuild
- sh: npm run dist:lin
- cmd: npm install
- cmd: npm run rebuild
- cmd: npm run dist:win:ci
- cmd: npm run reset
- cmd: npm run dist:cli
- cmd: 7z a ./dist-cli/bwdc-windows-%PACKAGE_VERSION%.zip ./dist-cli/windows/bwdc.exe ./keytar/windows/keytar.node
- cmd: 7z a ./dist-cli/bwdc-macos-%PACKAGE_VERSION%.zip ./dist-cli/macos/bwdc ./keytar/macos/keytar.node
- cmd: 7z a ./dist-cli/bwdc-linux-%PACKAGE_VERSION%.zip ./dist-cli/linux/bwdc ./keytar/linux/keytar.node
- ps: |
if($isWindows) {
Expand-Archive -Path "./dist-cli/bwdc-windows-${env:PACKAGE_VERSION}.zip" -DestinationPath "./test/windows"
$testVersion = Invoke-Expression '& ./test/windows/bwdc.exe -v'
if($testVersion -ne $env:PACKAGE_VERSION) {
Throw "Version test failed."
}
}
- ps: |
if($isWindows) {
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
}
- ps: |
if($isLinux) {
Push-AppveyorArtifact ./dist/Bitwarden-Connector-${env:PACKAGE_VERSION}-x86_64.AppImage
}
else {
Push-AppveyorArtifact .\dist\Bitwarden-Connector-Portable-${env:PACKAGE_VERSION}.exe
Push-AppveyorArtifact .\dist\Bitwarden-Connector-Installer-${env:PACKAGE_VERSION}.exe
Push-AppveyorArtifact .\dist-cli\bwdc-windows-${env:PACKAGE_VERSION}.zip
Push-AppveyorArtifact .\dist-cli\bwdc-macos-${env:PACKAGE_VERSION}.zip
Push-AppveyorArtifact .\dist-cli\bwdc-linux-${env:PACKAGE_VERSION}.zip
Push-AppveyorArtifact .\dist-cli\bwdc-windows-sha256-${env:PACKAGE_VERSION}.txt
Push-AppveyorArtifact .\dist-cli\bwdc-macos-sha256-${env:PACKAGE_VERSION}.txt
Push-AppveyorArtifact .\dist-cli\bwdc-linux-sha256-${env:PACKAGE_VERSION}.txt
}
on_finish:
- ps: |
if($isWindows -and $env:DEBUG_RDP -eq "true") {
$blockRdp = $true
iex ((new-object net.webclient).DownloadString(`
'https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1'))
}
for:
-
matrix:
only:
- image: Visual Studio 2017
cache:
- '%LOCALAPPDATA%\electron'
- '%LOCALAPPDATA%\electron-builder'
- 'C:\Users\appveyor\.pkg-cache\'
-
matrix:
only:
- image: Ubuntu1804
cache:
- '/home/appveyor/.cache/electron'
- '/home/appveyor/.cache/electron-builder'
deploy:
tag: $(APPVEYOR_REPO_TAG_NAME)
release: $(RELEASE_NAME)
provider: GitHub
auth_token: $(GH_TOKEN)
artifact: /.*\.(zip|txt)/,
force_update: true
on:
branch: master
APPVEYOR_REPO_TAG: true

31
gulpfile.js Normal file
View File

@@ -0,0 +1,31 @@
const gulp = require('gulp');
const googleWebFonts = require('gulp-google-webfonts');
const del = require('del');
const paths = {
cssDir: './src/css/',
};
function clean() {
return del([paths.cssDir]);
}
function webfonts() {
return gulp.src('./webfonts.list')
.pipe(googleWebFonts({
fontsDir: 'webfonts',
cssFilename: 'webfonts.css',
format: 'woff',
}))
.pipe(gulp.dest(paths.cssDir));
}
// ref: https://github.com/angular/angular/issues/22524
function cleanupAotIssue() {
return del(['./node_modules/@types/uglify-js/node_modules/source-map/source-map.d.ts']);
}
exports.clean = clean;
exports.cleanupAotIssue = cleanupAotIssue;
exports.webfonts = gulp.series(clean, webfonts);
exports['prebuild:renderer'] = gulp.parallel(webfonts, cleanupAotIssue);;

2
jslib

Submodule jslib updated: e0d5a4d8b7...a884f77938

33
make-versioninfo.ps1 Normal file
View File

@@ -0,0 +1,33 @@
$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", "8bit Solutions LLC"
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 8bit Solutions LLC"
}
}
BLOCK "VarFileInfo"
{
VALUE "Translation", 0x0409 0x04B0
}
}
"@
$versionInfo | Out-File ./version-info.rc

13705
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -21,26 +21,42 @@
"sub:update": "git submodule update --remote", "sub:update": "git submodule update --remote",
"sub:pull": "git submodule foreach git pull origin master", "sub:pull": "git submodule foreach git pull origin master",
"sub:commit": "npm run sub:pull && git commit -am \"update submodule\"", "sub:commit": "npm run sub:pull && git commit -am \"update submodule\"",
"postinstall": "./node_modules/.bin/electron-rebuild && npm run sub:init", "postinstall": "npm run sub:init",
"rebuild": "./node_modules/.bin/electron-rebuild",
"reset": "rimraf ./node_modules/keytar/* && npm install",
"lint": "tslint src/**/*.ts || true", "lint": "tslint src/**/*.ts || true",
"lint:fix": "tslint src/**/*.ts --fix", "lint:fix": "tslint src/**/*.ts --fix",
"build": "concurrently -n Main,Rend -c yellow,cyan \"npm run build:main\" \"npm run build:renderer\"", "build": "concurrently -n Main,Rend -c yellow,cyan \"npm run build:main\" \"npm run build:renderer\"",
"build:main": "webpack --config webpack.main.js", "build:main": "webpack --config webpack.main.js",
"build:renderer": "webpack --config webpack.renderer.js", "build:renderer": "gulp prebuild:renderer && webpack --config webpack.renderer.js",
"build:renderer:watch": "webpack --config webpack.renderer.js --watch", "build:renderer:watch": "gulp prebuild:renderer && 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\"", "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/*", "clean:dist": "rimraf ./dist/*",
"clean:dist:cli": "rimraf ./dist-cli/*",
"pack:lin": "npm run clean:dist && build --linux --x64 -p never", "pack:lin": "npm run clean:dist && build --linux --x64 -p never",
"pack:mac": "npm run clean:dist && build --mac -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": "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", "pack:win:ci": "npm run clean:dist && build --win --x64 --ia32 -p never",
"dist:lin": "npm run build && npm run pack:lin", "pack:cli": "npm run pack:cli:win | npm run pack:cli:mac | npm run pack:cli:lin",
"dist:mac": "npm run build && npm run pack:mac", "pack:cli:win": "pkg . --targets win-x64 --output ./dist-cli/windows/bwdc.exe",
"dist:win": "npm run build && npm run pack:win", "pack:cli:mac": "pkg . --targets macos-x64 --output ./dist-cli/macos/bwdc",
"pack:cli:lin": "pkg . --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", "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", "dist:cli": "npm run build:cli:prod && npm run clean:dist:cli && npm run pack:cli",
"publish:mac": "npm run build && npm run clean:dist && build --mac -p always", "dist:cli:win": "npm run build:cli:prod && npm run clean:dist:cli && npm run pack:cli:win",
"publish:win": "npm run build && npm run clean:dist && build --win --x64 --ia32 -p always -c.win.certificateSubjectName=\"8bit Solutions LLC\"" "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 && build --linux --x64 -p always",
"publish:mac": "npm run build:dist && npm run clean:dist && build --mac -p always",
"publish:win": "npm run build:dist && npm run clean:dist && build --win --x64 --ia32 -p always -c.win.certificateSubjectName=\"8bit Solutions LLC\""
}, },
"build": { "build": {
"appId": "com.bitwarden.directory-connector", "appId": "com.bitwarden.directory-connector",
@@ -106,69 +122,97 @@
"artifactName": "Bitwarden-Connector-${version}-${arch}.${ext}" "artifactName": "Bitwarden-Connector-${version}-${arch}.${ext}"
} }
}, },
"bin": {
"bwdc": "./build-cli/bwdc.js"
},
"pkg": {
"assets": "./build-cli/**/*"
},
"devDependencies": { "devDependencies": {
"@angular/compiler-cli": "5.2.0", "@angular/compiler-cli": "^7.2.1",
"@microsoft/microsoft-graph-types": "^1.2.0", "@microsoft/microsoft-graph-types": "^1.4.0",
"@ngtools/webpack": "1.10.2", "@ngtools/webpack": "^7.2.2",
"@types/keytar": "^4.0.1", "@types/commander": "^2.12.2",
"@types/form-data": "^2.2.1",
"@types/inquirer": "^0.0.43",
"@types/ldapjs": "^1.0.3", "@types/ldapjs": "^1.0.3",
"@types/lowdb": "^1.0.1", "@types/lowdb": "^1.0.5",
"@types/lunr": "2.1.5", "@types/lunr": "^2.1.6",
"@types/node": "8.0.19", "@types/node": "^10.9.4",
"@types/node-forge": "0.7.1", "@types/node-fetch": "^2.1.2",
"@types/webcrypto": "0.0.28", "@types/node-forge": "^0.7.5",
"clean-webpack-plugin": "^0.1.17", "@types/papaparse": "^4.5.3",
"concurrently": "3.5.1", "@types/semver": "^5.5.0",
"copy-webpack-plugin": "^4.2.0", "@types/source-map": "0.5.2",
"css-loader": "^0.28.7", "@types/webcrypto": "^0.0.28",
"electron": "2.0.2", "@types/webpack": "^4.4.11",
"electron-builder": "^20.8.1", "@types/zxcvbn": "4.4.0",
"electron-rebuild": "1.7.3", "clean-webpack-plugin": "^0.1.19",
"electron-reload": "1.2.2", "concurrently": "^4.0.1",
"extract-text-webpack-plugin": "^3.0.1", "copy-webpack-plugin": "^4.5.2",
"file-loader": "^1.1.5", "cross-env": "^5.2.0",
"css-loader": "^1.0.0",
"del": "^3.0.0",
"electron": "3.0.14",
"electron-builder": "20.38.5",
"electron-rebuild": "^1.8.2",
"electron-reload": "^1.4.0",
"extract-text-webpack-plugin": "next",
"file-loader": "^2.0.0",
"font-awesome": "4.7.0", "font-awesome": "4.7.0",
"google-fonts-webpack-plugin": "^0.4.4", "gulp": "^4.0.0",
"html-loader": "^0.5.1", "gulp-google-webfonts": "^2.0.0",
"html-webpack-plugin": "^2.30.1", "html-loader": "^0.5.5",
"html-webpack-plugin": "^3.2.0",
"node-abi": "^2.5.1",
"node-loader": "^0.6.0", "node-loader": "^0.6.0",
"node-sass": "^4.7.2", "node-sass": "^4.9.3",
"pkg": "4.3.4",
"rimraf": "^2.6.2", "rimraf": "^2.6.2",
"sass-loader": "^6.0.6", "sass-loader": "^7.1.0",
"ts-loader": "^3.5.0", "ts-loader": "^5.3.3",
"tslint": "^5.9.1", "tslint": "^5.12.1",
"tslint-loader": "^3.5.3", "tslint-loader": "^3.5.4",
"typescript": "^2.7.1", "typescript": "3.2.4",
"webpack": "^3.10.0", "webpack": "^4.29.0",
"webpack-merge": "^4.1.0", "webpack-cli": "^3.2.1",
"webpack-node-externals": "^1.6.0" "webpack-merge": "^4.2.1",
"webpack-node-externals": "^1.7.2"
}, },
"dependencies": { "dependencies": {
"@angular/animations": "5.2.0", "@angular/animations": "7.2.1",
"@angular/common": "5.2.0", "@angular/common": "7.2.1",
"@angular/compiler": "5.2.0", "@angular/compiler": "7.2.1",
"@angular/core": "5.2.0", "@angular/core": "7.2.1",
"@angular/forms": "5.2.0", "@angular/forms": "7.2.1",
"@angular/http": "5.2.0", "@angular/http": "7.2.1",
"@angular/platform-browser": "5.2.0", "@angular/platform-browser": "7.2.1",
"@angular/platform-browser-dynamic": "5.2.0", "@angular/platform-browser-dynamic": "7.2.1",
"@angular/router": "5.2.0", "@angular/router": "7.2.1",
"@angular/upgrade": "5.2.0", "@angular/upgrade": "7.2.1",
"@microsoft/microsoft-graph-client": "1.0.0", "@microsoft/microsoft-graph-client": "1.2.0",
"@okta/okta-sdk-nodejs": "1.1.0", "@okta/okta-sdk-nodejs": "1.2.0",
"angular2-toaster": "4.0.2", "angular2-toaster": "6.1.0",
"angulartics2": "5.0.1", "angulartics2": "6.3.0",
"bootstrap": "4.1.0", "big-integer": "1.6.36",
"core-js": "2.4.1", "bootstrap": "4.1.3",
"electron-log": "2.2.14", "chalk": "2.4.1",
"electron-updater": "2.21.4", "commander": "2.18.0",
"googleapis": "29.0.0", "core-js": "2.6.2",
"keytar": "4.1.0", "duo_web_sdk": "git+https://github.com/duosecurity/duo_web_sdk.git",
"ldapjs": "1.0.2", "electron-log": "2.2.17",
"electron-store": "1.3.0",
"electron-updater": "4.0.6",
"form-data": "2.3.2",
"googleapis": "33.0.0",
"inquirer": "6.2.0",
"keytar": "4.4.1",
"ldapjs": "git+https://git@github.com/kspearrin/node-ldapjs.git",
"lowdb": "1.0.0", "lowdb": "1.0.0",
"lunr": "2.1.6", "lunr": "2.3.3",
"node-forge": "0.7.1", "node-fetch": "2.2.0",
"rxjs": "5.5.6", "node-forge": "0.7.6",
"zone.js": "0.8.19" "rxjs": "6.3.3",
"zone.js": "0.8.28"
} }
} }

View File

@@ -23,7 +23,8 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="identityUrl">{{'identityUrl' | i18n}}</label> <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> </div>
<div class="modal-footer justify-content-start"> <div class="modal-footer justify-content-start">

View File

@@ -1,10 +1,8 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { EnvironmentService } from 'jslib/abstractions/environment.service'; import { EnvironmentService } from 'jslib/abstractions/environment.service';
import { I18nService } from 'jslib/abstractions/i18n.service'; import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { EnvironmentComponent as BaseEnvironmentComponent } from 'jslib/angular/components/environment.component'; import { EnvironmentComponent as BaseEnvironmentComponent } from 'jslib/angular/components/environment.component';
@@ -13,8 +11,8 @@ import { EnvironmentComponent as BaseEnvironmentComponent } from 'jslib/angular/
templateUrl: 'environment.component.html', templateUrl: 'environment.component.html',
}) })
export class EnvironmentComponent extends BaseEnvironmentComponent { export class EnvironmentComponent extends BaseEnvironmentComponent {
constructor(analytics: Angulartics2, toasterService: ToasterService, constructor(environmentService: EnvironmentService, i18nService: I18nService,
environmentService: EnvironmentService, i18nService: I18nService) { platformUtilsService: PlatformUtilsService) {
super(analytics, toasterService, environmentService, i18nService); super(platformUtilsService, environmentService, i18nService);
} }
} }

View File

@@ -9,12 +9,13 @@
<div class="card-body"> <div class="card-body">
<div class="form-group"> <div class="form-group">
<label for="email">{{'emailAddress' | i18n}}</label> <label for="email">{{'emailAddress' | i18n}}</label>
<input id="email" type="text" name="Email" [(ngModel)]="email" class="form-control" appAutoFocus> <input id="email" type="text" name="Email" [(ngModel)]="email" class="form-control">
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="row-main"> <div class="row-main">
<label for="masterPassword">{{'masterPass' | i18n}}</label> <label for="masterPassword">{{'masterPass' | i18n}}</label>
<input id="masterPassword" type="password" name="MasterPassword" [(ngModel)]="masterPassword" class="form-control"> <input id="masterPassword" type="password" name="MasterPassword"
[(ngModel)]="masterPassword" class="form-control">
</div> </div>
</div> </div>
<button type="submit" class="btn btn-primary" [disabled]="form.loading"> <button type="submit" class="btn btn-primary" [disabled]="form.loading">

View File

@@ -6,14 +6,12 @@ import {
} from '@angular/core'; } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { EnvironmentComponent } from './environment.component'; import { EnvironmentComponent } from './environment.component';
import { AuthService } from 'jslib/abstractions/auth.service'; import { AuthService } from 'jslib/abstractions/auth.service';
import { I18nService } from 'jslib/abstractions/i18n.service'; import { I18nService } from 'jslib/abstractions/i18n.service';
import { SyncService } from 'jslib/abstractions/sync.service'; import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { StorageService } from 'jslib/abstractions/storage.service';
import { LoginComponent as BaseLoginComponent } from 'jslib/angular/components/login.component'; import { LoginComponent as BaseLoginComponent } from 'jslib/angular/components/login.component';
import { ModalComponent } from 'jslib/angular/components/modal.component'; import { ModalComponent } from 'jslib/angular/components/modal.component';
@@ -26,9 +24,9 @@ export class LoginComponent extends BaseLoginComponent {
@ViewChild('environment', { read: ViewContainerRef }) environmentModal: ViewContainerRef; @ViewChild('environment', { read: ViewContainerRef }) environmentModal: ViewContainerRef;
constructor(authService: AuthService, router: Router, constructor(authService: AuthService, router: Router,
analytics: Angulartics2, toasterService: ToasterService, i18nService: I18nService, private componentFactoryResolver: ComponentFactoryResolver,
i18nService: I18nService, private componentFactoryResolver: ComponentFactoryResolver) { storageService: StorageService, platformUtilsService: PlatformUtilsService) {
super(authService, router, analytics, toasterService, i18nService); super(authService, router, platformUtilsService, i18nService, storageService);
super.successRoute = '/tabs/dashboard'; super.successRoute = '/tabs/dashboard';
} }

View File

@@ -9,7 +9,7 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<p *ngFor="let p of providers"> <p *ngFor="let p of providers">
<a href="#" (click)="choose(p)"> <a href="#" appStopClick (click)="choose(p)">
<strong>{{p.name}}</strong> <strong>{{p.name}}</strong>
</a> </a>
<br /> {{p.description}} <br /> {{p.description}}

View File

@@ -1,9 +1,6 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { AuthService } from 'jslib/abstractions/auth.service'; import { AuthService } from 'jslib/abstractions/auth.service';
import { I18nService } from 'jslib/abstractions/i18n.service'; import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service'; import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
@@ -18,8 +15,7 @@ import {
}) })
export class TwoFactorOptionsComponent extends BaseTwoFactorOptionsComponent { export class TwoFactorOptionsComponent extends BaseTwoFactorOptionsComponent {
constructor(authService: AuthService, router: Router, constructor(authService: AuthService, router: Router,
analytics: Angulartics2, toasterService: ToasterService,
i18nService: I18nService, platformUtilsService: PlatformUtilsService) { i18nService: I18nService, platformUtilsService: PlatformUtilsService) {
super(authService, router, analytics, toasterService, i18nService, platformUtilsService, window); super(authService, router, i18nService, platformUtilsService, window);
} }
} }

View File

@@ -5,7 +5,8 @@
<div class="card"> <div class="card">
<h5 class="card-header">{{title}}</h5> <h5 class="card-header">{{title}}</h5>
<div class="card-body"> <div class="card-body">
<ng-container *ngIf="selectedProviderType === providerType.Email || selectedProviderType === providerType.Authenticator"> <ng-container
*ngIf="selectedProviderType === providerType.Email || selectedProviderType === providerType.Authenticator">
<p *ngIf="selectedProviderType === providerType.Authenticator"> <p *ngIf="selectedProviderType === providerType.Authenticator">
{{'enterVerificationCodeApp' | i18n}} {{'enterVerificationCodeApp' | i18n}}
</p> </p>
@@ -14,15 +15,17 @@
</p> </p>
<div class="form-group"> <div class="form-group">
<label for="code">{{'verificationCode' | i18n}}</label> <label for="code">{{'verificationCode' | i18n}}</label>
<input id="code" type="text" name="Code" [(ngModel)]="token" appAutofocus class="form-control"> <input id="code" type="text" name="Code" [(ngModel)]="token" appAutofocus
class="form-control">
</div> </div>
</ng-container> </ng-container>
<ng-container *ngIf="selectedProviderType === providerType.Yubikey"> <ng-container *ngIf="selectedProviderType === providerType.Yubikey">
<p>{{'insertYubiKey' | i18n}}</p> <p>{{'insertYubiKey' | i18n}}</p>
<img src="../../images/yubikey.jpg" alt=""> <p><img src="../../images/yubikey.jpg" class="img-fluid rounded" alt=""></p>
<div class="form-group"> <div class="form-group">
<label for="code">{{'verificationCode' | i18n}}</label> <label for="code">{{'verificationCode' | i18n}}</label>
<input id="code" type="password" name="Code" [(ngModel)]="token" appAutofocus class="form-control"> <input id="code" type="password" name="Code" [(ngModel)]="token" appAutofocus
class="form-control">
</div> </div>
</ng-container> </ng-container>
<ng-container *ngIf="selectedProviderType === providerType.Duo || <ng-container *ngIf="selectedProviderType === providerType.Duo ||
@@ -31,13 +34,16 @@
<iframe id="duo_iframe"></iframe> <iframe id="duo_iframe"></iframe>
</div> </div>
</ng-container> </ng-container>
<div class="form-group" *ngIf="selectedProviderType !== null && selectedProviderType !== providerType.U2f"> <div class="form-group"
*ngIf="selectedProviderType != null && selectedProviderType !== providerType.U2f">
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="remember" [(ngModel)]="remember" name="Remember"> <input class="form-check-input" type="checkbox" id="remember" [(ngModel)]="remember"
name="Remember">
<label class="form-check-label" for="remember">{{'rememberMe' | i18n}}</label> <label class="form-check-label" for="remember">{{'rememberMe' | i18n}}</label>
</div> </div>
</div> </div>
<ng-container class="card-body" *ngIf="selectedProviderType === null || selectedProviderType === providerType.U2f"> <ng-container class="card-body"
*ngIf="selectedProviderType === null || selectedProviderType === providerType.U2f">
<p>{{'noTwoStepProviders' | i18n}}</p> <p>{{'noTwoStepProviders' | i18n}}</p>
<p>{{'noTwoStepProviders2' | i18n}}</p> <p>{{'noTwoStepProviders2' | i18n}}</p>
</ng-container> </ng-container>
@@ -52,7 +58,8 @@
</div> </div>
<div class="text-center mt-3"> <div class="text-center mt-3">
<a href="#" appStopClick (click)="anotherMethod()">{{'useAnotherTwoStepMethod' | i18n}}</a> <a href="#" appStopClick (click)="anotherMethod()">{{'useAnotherTwoStepMethod' | i18n}}</a>
<a href="#" appStopClick (click)="sendEmail(true)" [appApiAction]="emailPromise" *ngIf="selectedProviderType === providerType.Email"> <a href="#" appStopClick (click)="sendEmail(true)" [appApiAction]="emailPromise"
*ngIf="selectedProviderType === providerType.Email">
{{'sendVerificationCodeEmailAgain' | i18n}} {{'sendVerificationCodeEmailAgain' | i18n}}
</a> </a>
</div> </div>

View File

@@ -7,9 +7,6 @@ import {
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { TwoFactorOptionsComponent } from './two-factor-options.component'; import { TwoFactorOptionsComponent } from './two-factor-options.component';
import { TwoFactorProviderType } from 'jslib/enums/twoFactorProviderType'; import { TwoFactorProviderType } from 'jslib/enums/twoFactorProviderType';
@@ -19,7 +16,6 @@ import { AuthService } from 'jslib/abstractions/auth.service';
import { EnvironmentService } from 'jslib/abstractions/environment.service'; import { EnvironmentService } from 'jslib/abstractions/environment.service';
import { I18nService } from 'jslib/abstractions/i18n.service'; import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service'; import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { SyncService } from 'jslib/abstractions/sync.service';
import { ModalComponent } from 'jslib/angular/components/modal.component'; import { ModalComponent } from 'jslib/angular/components/modal.component';
import { TwoFactorComponent as BaseTwoFactorComponent } from 'jslib/angular/components/two-factor.component'; import { TwoFactorComponent as BaseTwoFactorComponent } from 'jslib/angular/components/two-factor.component';
@@ -32,12 +28,10 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
@ViewChild('twoFactorOptions', { read: ViewContainerRef }) twoFactorOptionsModal: ViewContainerRef; @ViewChild('twoFactorOptions', { read: ViewContainerRef }) twoFactorOptionsModal: ViewContainerRef;
constructor(authService: AuthService, router: Router, constructor(authService: AuthService, router: Router,
analytics: Angulartics2, toasterService: ToasterService,
i18nService: I18nService, apiService: ApiService, i18nService: I18nService, apiService: ApiService,
platformUtilsService: PlatformUtilsService, environmentService: EnvironmentService, platformUtilsService: PlatformUtilsService, environmentService: EnvironmentService,
private componentFactoryResolver: ComponentFactoryResolver) { private componentFactoryResolver: ComponentFactoryResolver) {
super(authService, router, analytics, toasterService, i18nService, apiService, super(authService, router, i18nService, apiService, platformUtilsService, window, environmentService);
platformUtilsService, window, environmentService);
super.successRoute = '/tabs/dashboard'; super.successRoute = '/tabs/dashboard';
} }

View File

@@ -1,24 +1,26 @@
import { import {
BodyOutputType,
Toast,
ToasterConfig, ToasterConfig,
ToasterContainerComponent, ToasterContainerComponent,
ToasterService,
} from 'angular2-toaster'; } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { Angulartics2GoogleAnalytics } from 'angulartics2/ga'; import { Angulartics2GoogleAnalytics } from 'angulartics2/ga';
import { import {
Component, Component,
ComponentFactoryResolver, ComponentFactoryResolver,
NgZone, NgZone,
OnDestroy,
OnInit, OnInit,
SecurityContext,
Type, Type,
ViewChild, ViewChild,
ViewContainerRef, ViewContainerRef,
} from '@angular/core'; } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ModalComponent } from 'jslib/angular/components/modal.component'; import { ModalComponent } from 'jslib/angular/components/modal.component';
import { BroadcasterService } from 'jslib/angular/services/broadcaster.service'; import { BroadcasterService } from 'jslib/angular/services/broadcaster.service';
@@ -27,14 +29,11 @@ import { ApiService } from 'jslib/abstractions/api.service';
import { AuthService } from 'jslib/abstractions/auth.service'; import { AuthService } from 'jslib/abstractions/auth.service';
import { I18nService } from 'jslib/abstractions/i18n.service'; import { I18nService } from 'jslib/abstractions/i18n.service';
import { MessagingService } from 'jslib/abstractions/messaging.service'; import { MessagingService } from 'jslib/abstractions/messaging.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { StateService } from 'jslib/abstractions/state.service'; import { StateService } from 'jslib/abstractions/state.service';
import { StorageService } from 'jslib/abstractions/storage.service'; import { StorageService } from 'jslib/abstractions/storage.service';
import { TokenService } from 'jslib/abstractions/token.service'; import { TokenService } from 'jslib/abstractions/token.service';
import { UserService } from 'jslib/abstractions/user.service'; import { UserService } from 'jslib/abstractions/user.service';
import { ConstantsService } from 'jslib/services/constants.service';
import { ConfigurationService } from '../services/configuration.service'; import { ConfigurationService } from '../services/configuration.service';
import { SyncService } from '../services/sync.service'; import { SyncService } from '../services/sync.service';
@@ -66,12 +65,12 @@ export class AppComponent implements OnInit {
private tokenService: TokenService, private storageService: StorageService, private tokenService: TokenService, private storageService: StorageService,
private authService: AuthService, private router: Router, private analytics: Angulartics2, private authService: AuthService, private router: Router, private analytics: Angulartics2,
private toasterService: ToasterService, private i18nService: I18nService, 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 componentFactoryResolver: ComponentFactoryResolver, private messagingService: MessagingService,
private configurationService: ConfigurationService, private syncService: SyncService, private configurationService: ConfigurationService, private syncService: SyncService,
private stateService: StateService, private apiService: ApiService) { private stateService: StateService, private apiService: ApiService) {
(window as any).BitwardenToasterService = toasterService; (window as any).BitwardenToasterService = toasterService;
} }
ngOnInit() { ngOnInit() {
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => { this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
@@ -125,6 +124,15 @@ export class AppComponent implements OnInit {
this.messagingService.send('scheduleNextDirSync'); this.messagingService.send('scheduleNextDirSync');
break; break;
case 'showToast':
this.showToast(message);
break;
case 'analyticsEventTrack':
this.analytics.eventTrack.next({
action: message.action,
properties: { label: message.label },
});
break;
default: default:
} }
}); });
@@ -166,4 +174,31 @@ export class AppComponent implements OnInit {
this.modal = null; this.modal = null;
}); });
} }
private showToast(msg: any) {
const toast: Toast = {
type: msg.type,
title: msg.title,
};
if (typeof (msg.text) === 'string') {
toast.body = msg.text;
} else if (msg.text.length === 1) {
toast.body = msg.text[0];
} else {
let message = '';
msg.text.forEach((t: string) =>
message += ('<p>' + this.sanitizer.sanitize(SecurityContext.HTML, t) + '</p>'));
toast.body = message;
toast.bodyOutputType = BodyOutputType.TrustedHtml;
}
if (msg.options != null) {
if (msg.options.trustedHtml === true) {
toast.bodyOutputType = BodyOutputType.TrustedHtml;
}
if (msg.options.timeout != null && msg.options.timeout > 0) {
toast.timeout = msg.options.timeout;
}
}
this.toasterService.popAsync(toast);
}
} }

View File

@@ -50,7 +50,7 @@ import { SearchCiphersPipe } from 'jslib/angular/pipes/search-ciphers.pipe';
clearQueryParams: true, clearQueryParams: true,
}, },
}), }),
ToasterModule, ToasterModule.forRoot(),
], ],
declarations: [ declarations: [
ApiActionDirective, ApiActionDirective,

16
src/app/dummy.module.ts Normal file
View File

@@ -0,0 +1,16 @@
import { NgModule } from '@angular/core';
import { InputVerbatimDirective } from 'jslib/angular/directives/input-verbatim.directive';
import { TrueFalseValueDirective } from 'jslib/angular/directives/true-false-value.directive';
import { SearchPipe } from 'jslib/angular/pipes/search.pipe';
@NgModule({
imports: [],
declarations: [
InputVerbatimDirective,
TrueFalseValueDirective,
SearchPipe,
],
})
export class DummyModule {
}

View File

@@ -5,8 +5,6 @@ import { isDev } from 'jslib/electron/utils';
// tslint:disable-next-line // tslint:disable-next-line
require('../scss/styles.scss'); require('../scss/styles.scss');
// tslint:disable-next-line
require('../../jslib/src/misc/duo.js');
import { AppModule } from './app.module'; import { AppModule } from './app.module';
@@ -14,4 +12,4 @@ if (!isDev()) {
enableProdMode(); enableProdMode();
} }
platformBrowserDynamic().bootstrapModule(AppModule); platformBrowserDynamic().bootstrapModule(AppModule, { preserveWhitespaces: true });

View File

@@ -11,7 +11,7 @@ import { ElectronLogService } from 'jslib/electron/services/electronLog.service'
import { ElectronPlatformUtilsService } from 'jslib/electron/services/electronPlatformUtils.service'; import { ElectronPlatformUtilsService } from 'jslib/electron/services/electronPlatformUtils.service';
import { ElectronRendererMessagingService } from 'jslib/electron/services/electronRendererMessaging.service'; import { ElectronRendererMessagingService } from 'jslib/electron/services/electronRendererMessaging.service';
import { ElectronRendererSecureStorageService } from 'jslib/electron/services/electronRendererSecureStorage.service'; import { ElectronRendererSecureStorageService } from 'jslib/electron/services/electronRendererSecureStorage.service';
import { isDev } from 'jslib/electron/utils'; import { ElectronStorageService } from 'jslib/electron/services/electronStorage.service';
import { AuthGuardService } from './auth-guard.service'; import { AuthGuardService } from './auth-guard.service';
import { LaunchGuardService } from './launch-guard.service'; import { LaunchGuardService } from './launch-guard.service';
@@ -32,7 +32,6 @@ import { ConstantsService } from 'jslib/services/constants.service';
import { ContainerService } from 'jslib/services/container.service'; import { ContainerService } from 'jslib/services/container.service';
import { CryptoService } from 'jslib/services/crypto.service'; import { CryptoService } from 'jslib/services/crypto.service';
import { EnvironmentService } from 'jslib/services/environment.service'; import { EnvironmentService } from 'jslib/services/environment.service';
import { LowdbStorageService } from 'jslib/services/lowdbStorage.service';
import { NodeCryptoFunctionService } from 'jslib/services/nodeCryptoFunction.service'; import { NodeCryptoFunctionService } from 'jslib/services/nodeCryptoFunction.service';
import { StateService } from 'jslib/services/state.service'; import { StateService } from 'jslib/services/state.service';
import { TokenService } from 'jslib/services/token.service'; import { TokenService } from 'jslib/services/token.service';
@@ -56,10 +55,10 @@ import { UserService as UserServiceAbstraction } from 'jslib/abstractions/user.s
const logService = new ElectronLogService(); const logService = new ElectronLogService();
const i18nService = new I18nService(window.navigator.language, './locales'); const i18nService = new I18nService(window.navigator.language, './locales');
const stateService = new StateService(); const stateService = new StateService();
const platformUtilsService = new ElectronPlatformUtilsService(i18nService, true);
const broadcasterService = new BroadcasterService(); const broadcasterService = new BroadcasterService();
const messagingService = new ElectronRendererMessagingService(broadcasterService); const messagingService = new ElectronRendererMessagingService(broadcasterService);
const storageService: StorageServiceAbstraction = new LowdbStorageService(null, remote.app.getPath('userData')); const platformUtilsService = new ElectronPlatformUtilsService(i18nService, messagingService, true);
const storageService: StorageServiceAbstraction = new ElectronStorageService(remote.app.getPath('userData'));
const secureStorageService: StorageServiceAbstraction = new ElectronRendererSecureStorageService(); const secureStorageService: StorageServiceAbstraction = new ElectronRendererSecureStorageService();
const cryptoFunctionService: CryptoFunctionServiceAbstraction = new NodeCryptoFunctionService(); const cryptoFunctionService: CryptoFunctionServiceAbstraction = new NodeCryptoFunctionService();
const cryptoService = new CryptoService(storageService, secureStorageService, cryptoFunctionService); const cryptoService = new CryptoService(storageService, secureStorageService, cryptoFunctionService);
@@ -67,9 +66,9 @@ const appIdService = new AppIdService(storageService);
const tokenService = new TokenService(storageService); const tokenService = new TokenService(storageService);
const apiService = new ApiService(tokenService, platformUtilsService, const apiService = new ApiService(tokenService, platformUtilsService,
async (expired: boolean) => messagingService.send('logout', { expired: expired })); async (expired: boolean) => messagingService.send('logout', { expired: expired }));
const environmentService = new EnvironmentService(apiService, storageService); const environmentService = new EnvironmentService(apiService, storageService, null);
const userService = new UserService(tokenService, storageService); const userService = new UserService(tokenService, storageService);
const containerService = new ContainerService(cryptoService, platformUtilsService); const containerService = new ContainerService(cryptoService);
const authService = new AuthService(cryptoService, apiService, userService, tokenService, appIdService, const authService = new AuthService(cryptoService, apiService, userService, tokenService, appIdService,
i18nService, platformUtilsService, messagingService, false); i18nService, platformUtilsService, messagingService, false);
const configurationService = new ConfigurationService(storageService, secureStorageService); const configurationService = new ConfigurationService(storageService, secureStorageService);
@@ -78,14 +77,12 @@ const syncService = new SyncService(configurationService, logService, cryptoFunc
const analytics = new Analytics(window, () => true, platformUtilsService, storageService, appIdService); const analytics = new Analytics(window, () => true, platformUtilsService, storageService, appIdService);
containerService.attachToWindow(window); containerService.attachToWindow(window);
environmentService.setUrlsFromStorage().then(() => {
// Do nothing
});
export function initFactory(): Function { export function initFactory(): Function {
return async () => { return async () => {
await environmentService.setUrlsFromStorage();
await i18nService.init(); await i18nService.init();
await authService.init(); authService.init();
const htmlEl = window.document.documentElement; const htmlEl = window.document.documentElement;
htmlEl.classList.add('os_' + platformUtilsService.getDeviceString()); htmlEl.classList.add('os_' + platformUtilsService.getDeviceString());
htmlEl.classList.add('locale_' + i18nService.translationLocale); htmlEl.classList.add('locale_' + i18nService.translationLocale);

View File

@@ -14,7 +14,8 @@
<strong *ngIf="syncRunning" class="text-success">{{'running' | i18n}}</strong> <strong *ngIf="syncRunning" class="text-success">{{'running' | i18n}}</strong>
<strong *ngIf="!syncRunning" class="text-danger">{{'stopped' | i18n}}</strong> <strong *ngIf="!syncRunning" class="text-danger">{{'stopped' | i18n}}</strong>
</p> </p>
<button #startBtn (click)="start()" [appApiAction]="startPromise" class="btn btn-primary" [disabled]="startBtn.loading"> <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-play fa-fw" [hidden]="startBtn.loading"></i>
<i class="fa fa-spinner fa-fw fa-spin" [hidden]="!startBtn.loading"></i> <i class="fa fa-spinner fa-fw fa-spin" [hidden]="!startBtn.loading"></i>
{{'startSync' | i18n}} {{'startSync' | i18n}}
@@ -23,7 +24,8 @@
<i class="fa fa-stop fa-fw"></i> <i class="fa fa-stop fa-fw"></i>
{{'stopSync' | i18n}} {{'stopSync' | i18n}}
</button> </button>
<button #syncBtn (click)="sync()" [appApiAction]="syncPromise" class="btn btn-primary" [disabled]="syncBtn.loading"> <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> <i class="fa fa-refresh fa-fw" [ngClass]="{'fa-spin': syncBtn.loading}"></i>
{{'syncNow' | i18n}} {{'syncNow' | i18n}}
</button> </button>
@@ -33,7 +35,8 @@
<h3 class="card-header">{{'testing' | i18n}}</h3> <h3 class="card-header">{{'testing' | i18n}}</h3>
<div class="card-body"> <div class="card-body">
<p>{{'testingDesc' | i18n}}</p> <p>{{'testingDesc' | i18n}}</p>
<button #simBtn (click)="simulate()" [appApiAction]="simPromise" class="btn btn-primary" [disabled]="simBtn.loading"> <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-spinner fa-fw fa-spin" [hidden]="!simBtn.loading"></i>
<i class="fa fa-bug fa-fw" [hidden]="simBtn.loading"></i> <i class="fa fa-bug fa-fw" [hidden]="simBtn.loading"></i>
{{'testNow' | i18n}} {{'testNow' | i18n}}

View File

@@ -12,18 +12,17 @@ import { I18nService } from 'jslib/abstractions/i18n.service';
import { MessagingService } from 'jslib/abstractions/messaging.service'; import { MessagingService } from 'jslib/abstractions/messaging.service';
import { StateService } from 'jslib/abstractions/state.service'; import { StateService } from 'jslib/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 { SyncService } from '../../services/sync.service';
import { Entry } from '../../models/entry';
import { GroupEntry } from '../../models/groupEntry'; import { GroupEntry } from '../../models/groupEntry';
import { SimResult } from '../../models/simResult';
import { UserEntry } from '../../models/userEntry'; import { UserEntry } from '../../models/userEntry';
import { ConfigurationService } from '../../services/configuration.service'; 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'; const BroadcasterSubscriptionId = 'DashboardComponent';
@Component({ @Component({
@@ -36,7 +35,7 @@ export class DashboardComponent implements OnInit, OnDestroy {
simEnabledUsers: UserEntry[] = []; simEnabledUsers: UserEntry[] = [];
simDisabledUsers: UserEntry[] = []; simDisabledUsers: UserEntry[] = [];
simDeletedUsers: UserEntry[] = []; simDeletedUsers: UserEntry[] = [];
simPromise: Promise<any>; simPromise: Promise<SimResult>;
simSinceLast: boolean = false; simSinceLast: boolean = false;
syncPromise: Promise<[GroupEntry[], UserEntry[]]>; syncPromise: Promise<[GroupEntry[], UserEntry[]]>;
startPromise: Promise<any>; startPromise: Promise<any>;
@@ -103,63 +102,18 @@ export class DashboardComponent implements OnInit, OnDestroy {
this.simDisabledUsers = []; this.simDisabledUsers = [];
this.simDeletedUsers = []; this.simDeletedUsers = [];
this.simPromise = new Promise(async (resolve, reject) => { try {
try { this.simPromise = ConnectorUtils.simulate(this.syncService, this.i18nService, this.simSinceLast);
const result = await this.syncService.sync(!this.simSinceLast, true); const result = await this.simPromise;
if (result[0] != null) { this.simGroups = result.groups;
this.simGroups = result[0]; this.simUsers = result.users;
} this.simEnabledUsers = result.enabledUsers;
if (result[1] != null) { this.simDisabledUsers = result.disabledUsers;
this.simUsers = result[1]; this.simDeletedUsers = result.deletedUsers;
} } catch (e) {
} catch (e) { this.simGroups = null;
this.simGroups = null; this.simUsers = 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);
});
} }
private async updateLastSync() { private async updateLastSync() {

View File

@@ -12,7 +12,8 @@
<div [hidden]="directory != directoryType.Ldap"> <div [hidden]="directory != directoryType.Ldap">
<div class="form-group"> <div class="form-group">
<label for="hostname">{{'serverHostname' | i18n}}</label> <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> <small class="text-muted form-text">{{'ex' | i18n}} ad.company.com</small>
</div> </div>
<div class="form-group"> <div class="form-group">
@@ -22,37 +23,74 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="rootPath">{{'rootPath' | i18n}}</label> <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> <small class="text-muted form-text">{{'ex' | i18n}} dc=company,dc=com</small>
</div> </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-group">
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="ad" [(ngModel)]="ldap.ad" name="AD"> <input class="form-check-input" type="checkbox" id="ad" [(ngModel)]="ldap.ad" name="AD">
<label class="form-check-label" for="ad">{{'ldapAd' | i18n}}</label> <label class="form-check-label" for="ad">{{'ldapAd' | i18n}}</label>
</div> </div>
</div> </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="ml-4" *ngIf="ldap.ssl">
<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 class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="sslAllowUnauthorized"
[(ngModel)]="ldap.sslAllowUnauthorized" name="SSLAllowUnauthorized">
<label class="form-check-label"
for="sslAllowUnauthorized">{{'ldapSslAllowUnauthorized' | i18n}}</label>
</div>
</div>
</div>
<div class="form-group" [hidden]="true"> <div class="form-group" [hidden]="true">
<div class="form-check"> <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> <label class="form-check-label" for="currentUser">{{'currentUser' | i18n}}</label>
</div> </div>
</div> </div>
<div [hidden]="ldap.currentUser"> <div [hidden]="ldap.currentUser">
<div class="form-group"> <div class="form-group">
<label for="username">{{'username' | i18n}}</label> <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}} 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>
<div class="form-group"> <div class="form-group">
<label for="password">{{'password' | i18n}}</label> <label for="password">{{'password' | i18n}}</label>
<input type="password" class="form-control" id="password" name="Password" [(ngModel)]="ldap.password"> <input type="password" class="form-control" id="password" name="Password"
[(ngModel)]="ldap.password">
</div> </div>
</div> </div>
</div> </div>
@@ -64,7 +102,8 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="applicationId">{{'applicationId' | i18n}}</label> <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>
<div class="form-group"> <div class="form-group">
<label for="secretKey">{{'secretKey' | i18n}}</label> <label for="secretKey">{{'secretKey' | i18n}}</label>
@@ -79,7 +118,8 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="oktaToken">{{'token' | i18n}}</label> <label for="oktaToken">{{'token' | i18n}}</label>
<input type="text" class="form-control" id="oktaToken" name="OktaToken" [(ngModel)]="okta.token"> <input type="text" class="form-control" id="oktaToken" name="OktaToken"
[(ngModel)]="okta.token">
</div> </div>
</div> </div>
<div [hidden]="directory != directoryType.GSuite"> <div [hidden]="directory != directoryType.GSuite">
@@ -90,26 +130,31 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="adminUser">{{'adminUser' | i18n}}</label> <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> <small class="text-muted form-text">{{'ex' | i18n}} admin@company.com</small>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="customerId">{{'customerId' | i18n}}</label> <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> <small class="text-muted form-text">{{'ex' | i18n}} 39204722352</small>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="keyFile">{{'jsonKeyFile' | i18n}}</label> <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> <small class="text-muted form-text">{{'ex' | i18n}} My Project-jksd3jd223.json</small>
</div> </div>
<div class="form-group" [hidden]="!gsuite.clientEmail"> <div class="form-group" [hidden]="!gsuite.clientEmail">
<label for="clientEmail">{{'clientEmail' | i18n}}</label> <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>
<div class="form-group" [hidden]="!gsuite.privateKey"> <div class="form-group" [hidden]="!gsuite.privateKey">
<label for="privateKey">{{'privateKey' | i18n}}</label> <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> </textarea>
</div> </div>
</div> </div>
@@ -134,12 +179,14 @@
<div class="card-body"> <div class="card-body">
<div class="form-group"> <div class="form-group">
<label for="interval">{{'interval' | i18n}}</label> <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> <small class="text-muted form-text">{{'intervalMin' | i18n}}</small>
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="form-check"> <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> <label class="form-check-label" for="removeDisabled">{{'removeDisabled' | i18n}}</label>
</div> </div>
</div> </div>
@@ -147,36 +194,43 @@
<div [hidden]="ldap.ad"> <div [hidden]="ldap.ad">
<div class="form-group"> <div class="form-group">
<label for="memberAttribute">{{'memberAttribute' | i18n}}</label> <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> <small class="text-muted form-text">{{'ex' | i18n}} uniqueMember</small>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="creationDateAttribute">{{'creationDateAttribute' | i18n}}</label> <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> <small class="text-muted form-text">{{'ex' | i18n}} whenCreated</small>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="revisionDateAttribute">{{'revisionDateAttribute' | i18n}}</label> <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> <small class="text-muted form-text">{{'ex' | i18n}} whenChanged</small>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="useEmailPrefixSuffix" [(ngModel)]="sync.useEmailPrefixSuffix" name="UseEmailPrefixSuffix"> <input class="form-check-input" type="checkbox" id="useEmailPrefixSuffix"
<label class="form-check-label" for="useEmailPrefixSuffix">{{'useEmailPrefixSuffix' | i18n}}</label> [(ngModel)]="sync.useEmailPrefixSuffix" name="UseEmailPrefixSuffix">
<label class="form-check-label"
for="useEmailPrefixSuffix">{{'useEmailPrefixSuffix' | i18n}}</label>
</div> </div>
</div> </div>
<div [hidden]="!sync.useEmailPrefixSuffix"> <div [hidden]="!sync.useEmailPrefixSuffix">
<div class="form-group" [hidden]="ldap.ad"> <div class="form-group" [hidden]="ldap.ad">
<label for="emailPrefixAttribute">{{'emailPrefixAttribute' | i18n}}</label> <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> <small class="text-muted form-text">{{'ex' | i18n}} accountName</small>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="emailSuffix">{{'emailSuffix' | i18n}}</label> <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> <small class="text-muted form-text">{{'ex' | i18n}} @company.com</small>
</div> </div>
</div> </div>
@@ -184,33 +238,43 @@
<div class="form-group"> <div class="form-group">
<div class="form-check"> <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> <label class="form-check-label" for="syncUsers">{{'syncUsers' | i18n}}</label>
</div> </div>
</div> </div>
<div [hidden]="!sync.users"> <div [hidden]="!sync.users">
<div class="form-group"> <div class="form-group">
<label for="userFilter">{{'userFilter' | i18n}}</label> <label for="userFilter">{{'userFilter' | i18n}}</label>
<textarea class="form-control" id="userFilter" name="UserFilter" [(ngModel)]="sync.userFilter"></textarea> <textarea class="form-control" id="userFilter" name="UserFilter"
<small class="text-muted form-text" *ngIf="directory === directoryType.Ldap">{{'ex' | i18n}} (&amp;(givenName=John)(|(l=Dallas)(l=Austin)))</small> [(ngModel)]="sync.userFilter"></textarea>
<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.Ldap">{{'ex' | i18n}}
<small class="text-muted form-text" *ngIf="directory === directoryType.Okta">{{'ex' | i18n}} exclude:joe@company.com | profile.firstName eq "John"</small> (&amp;(givenName=John)(|(l=Dallas)(l=Austin)))</small>
<small class="text-muted form-text" *ngIf="directory === directoryType.GSuite">{{'ex' | i18n}} exclude:joe@company.com | orgName=Engineering</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>
<div class="form-group" [hidden]="directory != directoryType.Ldap"> <div class="form-group" [hidden]="directory != directoryType.Ldap">
<label for="userPath">{{'userPath' | i18n}}</label> <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> <small class="text-muted form-text">{{'ex' | i18n}} CN=Users</small>
</div> </div>
<div [hidden]="directory != directoryType.Ldap || ldap.ad"> <div [hidden]="directory != directoryType.Ldap || ldap.ad">
<div class="form-group"> <div class="form-group">
<label for="userObjectClass">{{'userObjectClass' | i18n}}</label> <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> <small class="text-muted form-text">{{'ex' | i18n}} inetOrgPerson</small>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="userEmailAttribute">{{'userEmailAttribute' | i18n}}</label> <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> <small class="text-muted form-text">{{'ex' | i18n}} mail</small>
</div> </div>
</div> </div>
@@ -218,34 +282,44 @@
<div class="form-group"> <div class="form-group">
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="syncGroups" [(ngModel)]="sync.groups" name="SyncGroups"> <input class="form-check-input" type="checkbox" id="syncGroups" [(ngModel)]="sync.groups"
name="SyncGroups">
<label class="form-check-label" for="syncGroups">{{'syncGroups' | i18n}}</label> <label class="form-check-label" for="syncGroups">{{'syncGroups' | i18n}}</label>
</div> </div>
</div> </div>
<div [hidden]="!sync.groups"> <div [hidden]="!sync.groups">
<div class="form-group"> <div class="form-group">
<label for="groupFilter">{{'groupFilter' | i18n}}</label> <label for="groupFilter">{{'groupFilter' | i18n}}</label>
<textarea class="form-control" id="groupFilter" name="GroupFilter" [(ngModel)]="sync.groupFilter"></textarea> <textarea class="form-control" id="groupFilter" name="GroupFilter"
<small class="text-muted form-text" *ngIf="directory === directoryType.Ldap">{{'ex' | i18n}} (&amp;!(name=Sales*)!(name=IT*))</small> [(ngModel)]="sync.groupFilter"></textarea>
<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.Ldap">{{'ex' | i18n}}
<small class="text-muted form-text" *ngIf="directory === directoryType.Okta">{{'ex' | i18n}} include:Sales,IT | type eq "APP_GROUP"</small> (&amp;!(name=Sales*)!(name=IT*))</small>
<small class="text-muted form-text" *ngIf="directory === directoryType.GSuite">{{'ex' | i18n}} include:Sales,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>
<div class="form-group" [hidden]="directory != directoryType.Ldap"> <div class="form-group" [hidden]="directory != directoryType.Ldap">
<label for="groupPath">{{'groupPath' | i18n}}</label> <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=Groups</small>
<small class="text-muted form-text" *ngIf="ldap.ad">{{'ex' | i18n}} CN=Users</small> <small class="text-muted form-text" *ngIf="ldap.ad">{{'ex' | i18n}} CN=Users</small>
</div> </div>
<div [hidden]="directory != directoryType.Ldap || ldap.ad"> <div [hidden]="directory != directoryType.Ldap || ldap.ad">
<div class="form-group"> <div class="form-group">
<label for="groupObjectClass">{{'groupObjectClass' | i18n}}</label> <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> <small class="text-muted form-text">{{'ex' | i18n}} groupOfUniqueNames</small>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="groupNameAttribute">{{'groupNameAttribute' | i18n}}</label> <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> <small class="text-muted form-text">{{'ex' | i18n}} name</small>
</div> </div>
</div> </div>

View File

@@ -21,6 +21,8 @@ import { LdapConfiguration } from '../../models/ldapConfiguration';
import { OktaConfiguration } from '../../models/oktaConfiguration'; import { OktaConfiguration } from '../../models/oktaConfiguration';
import { SyncConfiguration } from '../../models/syncConfiguration'; import { SyncConfiguration } from '../../models/syncConfiguration';
import { ConnectorUtils } from '../../utils';
@Component({ @Component({
selector: 'app-settings', selector: 'app-settings',
templateUrl: 'settings.component.html', templateUrl: 'settings.component.html',
@@ -76,32 +78,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
} }
async submit() { async submit() {
if (this.ldap.ad) { ConnectorUtils.adjustConfigForSave(this.ldap, this.sync);
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';
}
}
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.saveOrganizationId(this.organizationId);
await this.configurationService.saveDirectoryType(this.directory); await this.configurationService.saveDirectoryType(this.directory);
await this.configurationService.saveDirectory(DirectoryType.Ldap, this.ldap); await this.configurationService.saveDirectory(DirectoryType.Ldap, this.ldap);
@@ -111,7 +88,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
await this.configurationService.saveSync(this.sync); await this.configurationService.saveSync(this.sync);
} }
async parseKeyFile() { parseKeyFile() {
const filePicker = (document.getElementById('keyFile') as HTMLInputElement); const filePicker = (document.getElementById('keyFile') as HTMLInputElement);
if (filePicker.files == null || filePicker.files.length < 0) { if (filePicker.files == null || filePicker.files.length < 0) {
return; return;
@@ -122,7 +99,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
reader.onload = (evt) => { reader.onload = (evt) => {
this.ngZone.run(async () => { this.ngZone.run(async () => {
try { 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) { if (result.client_email != null && result.private_key != null) {
this.gsuite.clientEmail = result.client_email; this.gsuite.clientEmail = result.client_email;
this.gsuite.privateKey = result.private_key; this.gsuite.privateKey = result.private_key;
@@ -138,4 +115,18 @@ export class SettingsComponent implements OnInit, OnDestroy {
filePicker.value = ''; 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 = '';
}
} }

133
src/bwdc.ts Normal file
View File

@@ -0,0 +1,133 @@
import * as fs from 'fs';
import * as path from 'path';
import { LogLevelType } from 'jslib/enums/logLevelType';
import { AuthService } from 'jslib/services/auth.service';
import { ConfigurationService } from './services/configuration.service';
import { I18nService } from './services/i18n.service';
import { KeytarSecureStorageService } from './services/keytarSecureStorage.service';
import { SyncService } from './services/sync.service';
import { CliPlatformUtilsService } from 'jslib/cli/services/cliPlatformUtils.service';
import { ConsoleLogService } from 'jslib/cli/services/consoleLog.service';
import { AppIdService } from 'jslib/services/appId.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 { NodeApiService } from 'jslib/services/nodeApi.service';
import { NodeCryptoFunctionService } from 'jslib/services/nodeCryptoFunction.service';
import { NoopMessagingService } from 'jslib/services/noopMessaging.service';
import { TokenService } from 'jslib/services/token.service';
import { UserService } from 'jslib/services/user.service';
import { Program } from './program';
// tslint:disable-next-line
const packageJson = require('./package.json');
export class Main {
dataFilePath: string;
logService: ConsoleLogService;
messagingService: NoopMessagingService;
storageService: LowdbStorageService;
secureStorageService: KeytarSecureStorageService;
i18nService: I18nService;
platformUtilsService: CliPlatformUtilsService;
constantsService: ConstantsService;
cryptoService: CryptoService;
tokenService: TokenService;
appIdService: AppIdService;
apiService: NodeApiService;
environmentService: EnvironmentService;
userService: UserService;
containerService: ContainerService;
cryptoFunctionService: NodeCryptoFunctionService;
authService: AuthService;
configurationService: ConfigurationService;
syncService: SyncService;
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);
}
this.i18nService = new I18nService('en', './locales');
this.platformUtilsService = new CliPlatformUtilsService('connector', packageJson);
this.logService = new ConsoleLogService(this.platformUtilsService.isDev(),
(level) => process.env.BWCLI_DEBUG !== 'true' && level <= LogLevelType.Info);
this.cryptoFunctionService = new NodeCryptoFunctionService();
this.storageService = new LowdbStorageService(null, this.dataFilePath, true);
this.secureStorageService = new KeytarSecureStorageService(applicationName);
this.cryptoService = new CryptoService(this.storageService, this.secureStorageService,
this.cryptoFunctionService);
this.appIdService = new AppIdService(this.storageService);
this.tokenService = new TokenService(this.storageService);
this.messagingService = new NoopMessagingService();
this.apiService = new NodeApiService(this.tokenService, this.platformUtilsService,
async (expired: boolean) => await this.logout());
this.environmentService = new EnvironmentService(this.apiService, this.storageService, null);
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, true);
this.configurationService = new ConfigurationService(this.storageService, this.secureStorageService);
this.syncService = new SyncService(this.configurationService, this.logService, this.cryptoFunctionService,
this.apiService, this.messagingService, this.i18nService);
this.program = new Program(this);
}
async run() {
await this.init();
this.program.run();
}
async logout() {
await Promise.all([
this.tokenService.clearToken(),
this.userService.clear(),
]);
}
private async init() {
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 = this.platformUtilsService.getApplicationVersion();
if (installedVersion == null || installedVersion !== currentVersion) {
await this.storageService.save(ConstantsService.installedVersionKey, currentVersion);
}
}
}
const main = new Main();
main.run();

View File

@@ -0,0 +1,22 @@
import * as program from 'commander';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { ConfigurationService } from '../services/configuration.service';
import { Response } from 'jslib/cli/models/response';
import { MessageResponse } from 'jslib/cli/models/response/messageResponse';
export class ClearCacheCommand {
constructor(private configurationService: ConfigurationService, private i18nService: I18nService) { }
async run(cmd: program.Command): 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);
}
}
}

View File

@@ -0,0 +1,127 @@
import * as program from 'commander';
import { EnvironmentService } from 'jslib/abstractions/environment.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { ConfigurationService } from '../services/configuration.service';
import { DirectoryType } from '../enums/directoryType';
import { Response } from 'jslib/cli/models/response';
import { MessageResponse } from 'jslib/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 { SyncConfiguration } from '../models/syncConfiguration';
import { ConnectorUtils } from '../utils';
export class ConfigCommand {
private directory: DirectoryType;
private ldap = new LdapConfiguration();
private gsuite = new GSuiteConfiguration();
private azure = new AzureConfiguration();
private okta = new OktaConfiguration();
private sync = new SyncConfiguration();
constructor(private environmentService: EnvironmentService, private i18nService: I18nService,
private configurationService: ConfigurationService) { }
async run(setting: string, value: string, cmd: program.Command): Promise<Response> {
setting = setting.toLowerCase();
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;
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.Okta) {
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;
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 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.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.saveSync(this.sync);
}
}

View File

@@ -0,0 +1,29 @@
import * as program from 'commander';
import { ConfigurationService } from '../services/configuration.service';
import { Response } from 'jslib/cli/models/response';
import { StringResponse } from 'jslib/cli/models/response/stringResponse';
export class LastSyncCommand {
constructor(private configurationService: ConfigurationService) { }
async run(object: string, cmd: program.Command): 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);
}
}
}

View File

@@ -0,0 +1,25 @@
import * as program from 'commander';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { SyncService } from '../services/sync.service';
import { Response } from 'jslib/cli/models/response';
import { MessageResponse } from 'jslib/cli/models/response/messageResponse';
export class SyncCommand {
constructor(private syncService: SyncService, private i18nService: I18nService) { }
async run(cmd: program.Command): 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);
}
}
}

View File

@@ -0,0 +1,24 @@
import * as program from 'commander';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { SyncService } from '../services/sync.service';
import { ConnectorUtils } from '../utils';
import { Response } from 'jslib/cli/models/response';
import { TestResponse } from '../models/response/testResponse';
export class TestCommand {
constructor(private syncService: SyncService, private i18nService: I18nService) { }
async run(cmd: program.Command): 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);
}
}
}

1
src/global.d.ts vendored
View File

@@ -1,2 +1,3 @@
declare function escape(s: string): string; declare function escape(s: string): string;
declare function unescape(s: string): string; declare function unescape(s: string): string;
declare module 'duo_web_sdk';

View File

@@ -2,6 +2,8 @@
<html> <html>
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline';
img-src 'self' data: *; child-src *; frame-src *; connect-src *;">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Bitwarden Directory Connector</title> <title>Bitwarden Directory Connector</title>
<base href=""> <base href="">

View File

@@ -417,6 +417,21 @@
"ldapSsl": { "ldapSsl": {
"message": "This server uses SSL (LDAPS)" "message": "This server uses SSL (LDAPS)"
}, },
"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)"
},
"ldapSslAllowUnauthorized": {
"message": "Allow untrusted SSL connections (not recommended)."
},
"ldapAd": { "ldapAd": {
"message": "This server uses Active Directory" "message": "This server uses Active Directory"
}, },
@@ -501,6 +516,9 @@
"noGroups": { "noGroups": {
"message": "No groups to list." "message": "No groups to list."
}, },
"syncingComplete": {
"message": "Syncing complete."
},
"syncingStarted": { "syncingStarted": {
"message": "Syncing started." "message": "Syncing started."
}, },
@@ -550,5 +568,14 @@
}, },
"hideToTray": { "hideToTray": {
"message": "Hide to Tray" "message": "Hide to Tray"
},
"savedSetting": {
"message": "Saved setting `$SETTING_NAME$`.",
"placeholders": {
"setting_name": {
"content": "$1",
"example": "server"
}
}
} }
} }

View File

@@ -1,15 +1,14 @@
import { app, BrowserWindow } from 'electron'; import { app } from 'electron';
import * as path from 'path'; import * as path from 'path';
import { MenuMain } from './main/menu.main'; import { MenuMain } from './main/menu.main';
import { MessagingMain } from './main/messaging.main'; import { MessagingMain } from './main/messaging.main';
import { I18nService } from './services/i18n.service'; import { I18nService } from './services/i18n.service';
import { LowdbStorageService } from 'jslib/services/lowdbStorage.service';
import { KeytarStorageListener } from 'jslib/electron/keytarStorageListener'; import { KeytarStorageListener } from 'jslib/electron/keytarStorageListener';
import { ElectronLogService } from 'jslib/electron/services/electronLog.service'; import { ElectronLogService } from 'jslib/electron/services/electronLog.service';
import { ElectronMainMessagingService } from 'jslib/electron/services/electronMainMessaging.service'; import { ElectronMainMessagingService } from 'jslib/electron/services/electronMainMessaging.service';
import { ElectronStorageService } from 'jslib/electron/services/electronStorage.service';
import { TrayMain } from 'jslib/electron/tray.main'; import { TrayMain } from 'jslib/electron/tray.main';
import { UpdaterMain } from 'jslib/electron/updater.main'; import { UpdaterMain } from 'jslib/electron/updater.main';
import { WindowMain } from 'jslib/electron/window.main'; import { WindowMain } from 'jslib/electron/window.main';
@@ -17,7 +16,7 @@ import { WindowMain } from 'jslib/electron/window.main';
export class Main { export class Main {
logService: ElectronLogService; logService: ElectronLogService;
i18nService: I18nService; i18nService: I18nService;
storageService: LowdbStorageService; storageService: ElectronStorageService;
messagingService: ElectronMainMessagingService; messagingService: ElectronMainMessagingService;
keytarStorageListener: KeytarStorageListener; keytarStorageListener: KeytarStorageListener;
@@ -51,7 +50,7 @@ export class Main {
this.logService = new ElectronLogService(null, app.getPath('userData')); this.logService = new ElectronLogService(null, app.getPath('userData'));
this.i18nService = new I18nService('en', './locales/'); 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, 800, 600);
this.menuMain = new MenuMain(this); this.menuMain = new MenuMain(this);
@@ -61,7 +60,7 @@ export class Main {
this.messagingService.send('doneCheckingForUpdate'); this.messagingService.send('doneCheckingForUpdate');
}, () => { }, () => {
this.messagingService.send('doneCheckingForUpdate'); this.messagingService.send('doneCheckingForUpdate');
}); }, 'bitwardenDirectoryConnector');
this.trayMain = new TrayMain(this.windowMain, this.i18nService, this.storageService); this.trayMain = new TrayMain(this.windowMain, this.i18nService, this.storageService);
this.messagingMain = new MessagingMain(this.windowMain, this.menuMain, this.updaterMain, this.trayMain); 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) => {
@@ -72,7 +71,6 @@ export class Main {
} }
bootstrap() { bootstrap() {
this.storageService.init();
this.keytarStorageListener.init(); this.keytarStorageListener.init();
this.windowMain.init().then(async () => { this.windowMain.init().then(async () => {
await this.i18nService.init(app.getLocale()); await this.i18nService.init(app.getLocale());

View File

@@ -1,7 +1,9 @@
import { DirectoryType } from '../enums/directoryType';
export class LdapConfiguration { export class LdapConfiguration {
ssl = false; ssl = false;
sslAllowUnauthorized = false;
sslCertPath: string;
sslKeyPath: string;
sslCaPath: string;
hostname: string; hostname: string;
port = 389; port = 389;
domain: string; domain: string;

View 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);
}
}

View File

@@ -0,0 +1,22 @@
import { GroupResponse } from './groupResponse';
import { UserResponse } from './userResponse';
import { SimResult } from '../simResult';
import { BaseResponse } from 'jslib/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)) : [];
}
}

View 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
View 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[] = [];
}

303
src/package-lock.json generated
View File

@@ -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"
}
}
}
}

View File

@@ -2,19 +2,19 @@
"name": "bitwarden-directory-connector", "name": "bitwarden-directory-connector",
"productName": "Bitwarden Directory Connector", "productName": "Bitwarden Directory Connector",
"description": "Sync your user directory to your Bitwarden organization.", "description": "Sync your user directory to your Bitwarden organization.",
"version": "2.0.2", "version": "2.5.3",
"author": "8bit Solutions LLC <hello@bitwarden.com> (https://bitwarden.com)", "author": "8bit Solutions LLC <hello@bitwarden.com> (https://bitwarden.com)",
"homepage": "https://bitwarden.com", "homepage": "https://bitwarden.com",
"license": "GPL-3.0", "license": "GPL-3.0",
"main": "main.js", "main": "main.js",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/bitwarden/desktop" "url": "https://github.com/bitwarden/directory-connector"
}, },
"dependencies": { "dependencies": {
"electron-log": "2.2.14", "electron-log": "2.2.17",
"electron-updater": "2.21.4", "electron-store": "1.3.0",
"keytar": "4.1.0", "electron-updater": "4.0.6",
"lowdb": "1.0.0" "keytar": "4.4.1"
} }
} }

264
src/program.ts Normal file
View File

@@ -0,0 +1,264 @@
import * as chk 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/cli/commands/login.command';
import { LogoutCommand } from 'jslib/cli/commands/logout.command';
import { UpdateCommand } from 'jslib/cli/commands/update.command';
import { BaseProgram } from 'jslib/cli/baseProgram';
import { Response } from 'jslib/cli/models/response';
import { StringResponse } from 'jslib/cli/models/response/stringResponse';
const chalk = chk.default;
const writeLn = (s: string, finalLine: boolean = false) => {
if (finalLine && process.platform === 'win32') {
process.stdout.write(s);
} else {
process.stdout.write(s + '\n');
}
};
export class Program extends BaseProgram {
constructor(private main: Main) {
super(main.userService, writeLn);
}
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('--quiet', 'Don\'t return anything to stdout.')
.version(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('command:*', () => {
writeLn(chalk.redBright('Invalid command: ' + program.args.join(' ')));
writeLn('See --help for a list of available commands.', 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 [email] [password]')
.description('Log into a user account.')
.option('--method <method>', 'Two-step login method.')
.option('--code <code>', 'Two-step login code.')
.on('--help', () => {
writeLn('\n Notes:');
writeLn('');
writeLn(' See docs for valid `method` enum values.');
writeLn('');
writeLn(' Examples:');
writeLn('');
writeLn(' bw login');
writeLn(' bw login john@example.com myPassword321');
writeLn(' bw login john@example.com myPassword321 --method 1 --code 249213');
writeLn('', true);
})
.action(async (email: string, password: string, cmd: program.Command) => {
await this.exitIfAuthed();
const command = new LoginCommand(this.main.authService, this.main.apiService, this.main.i18nService);
const response = await command.run(email, password, cmd);
this.processResponse(response);
});
program
.command('logout')
.description('Log out of the current user account.')
.on('--help', () => {
writeLn('\n Examples:');
writeLn('');
writeLn(' bw logout');
writeLn('', true);
})
.action(async (cmd) => {
await this.exitIfNotAuthed();
const command = new LogoutCommand(this.main.authService, this.main.i18nService,
async () => await this.main.logout());
const response = await command.run(cmd);
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 (cmd) => {
await this.exitIfNotAuthed();
const command = new TestCommand(this.main.syncService, this.main.i18nService);
const response = await command.run(cmd);
this.processResponse(response);
});
program
.command('sync')
.description('Sync the directory.')
.on('--help', () => {
writeLn('\n Examples:');
writeLn('');
writeLn(' bwdc sync');
writeLn('', true);
})
.action(async (cmd) => {
await this.exitIfNotAuthed();
const command = new SyncCommand(this.main.syncService, this.main.i18nService);
const response = await command.run(cmd);
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, cmd: program.Command) => {
await this.exitIfNotAuthed();
const command = new LastSyncCommand(this.main.configurationService);
const response = await command.run(object, cmd);
this.processResponse(response);
});
program
.command('config <setting> <value>')
.description('Configure settings.')
.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('');
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 azure.key <key>');
writeLn(' bwdc config gsuite.key <key>');
writeLn(' bwdc config okta.token <token>');
writeLn('', true);
})
.action(async (setting, value, cmd) => {
const command = new ConfigCommand(this.main.environmentService, this.main.i18nService,
this.main.configurationService);
const response = await command.run(setting, value, cmd);
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 (cmd) => {
const command = new ClearCacheCommand(this.main.configurationService, this.main.i18nService);
const response = await command.run(cmd);
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 (cmd) => {
const command = new UpdateCommand(this.main.platformUtilsService, this.main.i18nService,
'directory-connector', 'bwdc', false);
const response = await command.run(cmd);
this.processResponse(response);
});
program
.parse(process.argv);
if (process.argv.slice(2).length === 0) {
program.outputHelp();
}
}
}

View File

@@ -35,6 +35,16 @@ $fa-font-path: "~font-awesome/fonts";
display: none; display: none;
} }
.toast-message {
p {
margin-bottom: 0.5rem;
&:last-child {
margin-bottom: 0;
}
}
}
&.toast-danger, &.toast-error { &.toast-danger, &.toast-error {
background-image: none !important; background-image: none !important;
background-color: $danger; background-color: $danger;

View File

@@ -1,4 +1,5 @@
@import "bootstrap.scss"; @import "../css/webfonts.css";
@import "bootstrap.scss";
@import "pages.scss"; @import "pages.scss";
@import "misc.scss"; @import "misc.scss";
@import "plugins.scss"; @import "plugins.scss";

View File

@@ -18,9 +18,15 @@ import { I18nService } from 'jslib/abstractions/i18n.service';
import { LogService } from 'jslib/abstractions/log.service'; import { LogService } from 'jslib/abstractions/log.service';
const NextLink = '@odata.nextLink'; const NextLink = '@odata.nextLink';
const DeltaLink = '@odata.deltaLink';
const ObjectType = '@odata.type'; const ObjectType = '@odata.type';
enum UserSetType {
IncludeUser,
ExcludeUser,
IncludeGroup,
ExcludeGroup,
}
export class AzureDirectoryService extends BaseDirectoryService implements DirectoryService { export class AzureDirectoryService extends BaseDirectoryService implements DirectoryService {
private client: graph.Client; private client: graph.Client;
private dirConfig: AzureConfiguration; private dirConfig: AzureConfiguration;
@@ -53,45 +59,34 @@ export class AzureDirectoryService extends BaseDirectoryService implements Direc
let users: UserEntry[]; let users: UserEntry[];
if (this.syncConfig.users) { if (this.syncConfig.users) {
users = await this.getUsers(force, !test); users = await this.getUsers();
} }
let groups: GroupEntry[]; let groups: GroupEntry[];
if (this.syncConfig.groups) { if (this.syncConfig.groups) {
const setFilter = this.createCustomSet(this.syncConfig.groupFilter); const setFilter = this.createCustomSet(this.syncConfig.groupFilter);
groups = await this.getGroups(this.forceGroup(force, users), !test, setFilter); groups = await this.getGroups(setFilter);
users = this.filterUsersFromGroupsSet(users, groups, setFilter); users = this.filterUsersFromGroupsSet(users, groups, setFilter);
} }
return [groups, users]; return [groups, users];
} }
private async getUsers(force: boolean, saveDelta: boolean): Promise<UserEntry[]> { private async getUsers(): Promise<UserEntry[]> {
const entryIds = new Set<string>();
const entries: UserEntry[] = []; const entries: UserEntry[] = [];
const userReq = this.client.api('/users');
let res: any = null; let res = await userReq.get();
const token = await this.configurationService.getUserDeltaToken(); const setFilter = this.createCustomUserSet(this.syncConfig.userFilter);
if (!force && token != null) {
try {
const deltaReq = this.client.api(token);
res = await deltaReq.get();
} catch {
res = null;
}
}
if (res == null) {
const userReq = this.client.api('/users/delta');
res = await userReq.get();
}
const setFilter = this.createCustomSet(this.syncConfig.userFilter);
while (true) { while (true) {
const users: graphType.User[] = res.value; const users: graphType.User[] = res.value;
if (users != null) { if (users != null) {
for (const user of users) { for (const user of users) {
if (user.id == null || entryIds.has(user.id)) {
continue;
}
const entry = this.buildUser(user); const entry = this.buildUser(user);
if (this.filterOutResult(setFilter, entry.email)) { if (await this.filterOutUserResult(setFilter, entry)) {
continue; continue;
} }
@@ -101,13 +96,11 @@ export class AzureDirectoryService extends BaseDirectoryService implements Direc
} }
entries.push(entry); entries.push(entry);
entryIds.add(user.id);
} }
} }
if (res[NextLink] == null) { if (res[NextLink] == null) {
if (res[DeltaLink] != null && saveDelta) {
await this.configurationService.saveUserDeltaToken(res[DeltaLink]);
}
break; break;
} else { } else {
const nextReq = this.client.api(res[NextLink]); const nextReq = this.client.api(res[NextLink]);
@@ -118,11 +111,93 @@ export class AzureDirectoryService extends BaseDirectoryService implements Direc
return entries; return entries;
} }
private createCustomUserSet(filter: string): [UserSetType, Set<string>] {
if (filter == null || filter === '') {
return null;
}
const mainParts = filter.split('|');
if (mainParts.length < 1 || mainParts[0] == null || mainParts[0].trim() === '') {
return null;
}
const parts = mainParts[0].split(':');
if (parts.length !== 2) {
return null;
}
const keyword = parts[0].trim().toLowerCase();
let userSetType = UserSetType.IncludeUser;
if (keyword === 'include') {
userSetType = UserSetType.IncludeUser;
} else if (keyword === 'exclude') {
userSetType = UserSetType.ExcludeUser;
} else if (keyword === 'includegroup') {
userSetType = UserSetType.IncludeGroup;
} else if (keyword === 'excludegroup') {
userSetType = UserSetType.ExcludeGroup;
} else {
return null;
}
const set = new Set<string>();
const pieces = parts[1].split(',');
for (const p of pieces) {
set.add(p.trim().toLowerCase());
}
return [userSetType, set];
}
private async filterOutUserResult(setFilter: [UserSetType, Set<string>], user: UserEntry): Promise<boolean> {
if (setFilter == null) {
return false;
}
let userSetTypeExclude = null;
if (setFilter[0] === UserSetType.IncludeUser) {
userSetTypeExclude = false;
} else if (setFilter[0] === UserSetType.ExcludeUser) {
userSetTypeExclude = true;
}
if (userSetTypeExclude != null) {
return this.filterOutResult([userSetTypeExclude, setFilter[1]], user.email);
}
try {
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;
}
} catch { }
return false;
}
private buildUser(user: graphType.User): UserEntry { private buildUser(user: graphType.User): UserEntry {
const entry = new UserEntry(); const entry = new UserEntry();
entry.referenceId = user.id; entry.referenceId = user.id;
entry.externalId = 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; entry.disabled = user.accountEnabled == null ? false : !user.accountEnabled;
if ((user as any)['@removed'] != null && (user as any)['@removed'].reason === 'changed') { if ((user as any)['@removed'] != null && (user as any)['@removed'].reason === 'changed') {
@@ -132,77 +207,25 @@ export class AzureDirectoryService extends BaseDirectoryService implements Direc
return entry; return entry;
} }
private async getGroups(force: boolean, saveDelta: boolean, private async getGroups(setFilter: [boolean, Set<string>]): Promise<GroupEntry[]> {
setFilter: [boolean, Set<string>]): Promise<GroupEntry[]> { const entryIds = new Set<string>();
const entries: GroupEntry[] = []; const entries: GroupEntry[] = [];
const changedGroupIds: string[] = []; const groupsReq = this.client.api('/groups');
const token = await this.configurationService.getGroupDeltaToken(); let res = await groupsReq.get();
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();
while (true) { while (true) {
const allGroups: graphType.Group[] = res.value; const groups: graphType.Group[] = res.value;
if (allGroups != null) { if (groups != null) {
for (const group of allGroups) { for (const group of groups) {
if (group.id == null || entryIds.has(group.id)) {
continue;
}
if (this.filterOutResult(setFilter, group.displayName)) { if (this.filterOutResult(setFilter, group.displayName)) {
continue; continue;
} }
const entry = await this.buildGroup(group); const entry = await this.buildGroup(group);
entries.push(entry); entries.push(entry);
entryIds.add(group.id);
} }
} }

View File

@@ -51,13 +51,13 @@ export abstract class BaseDirectoryService {
protected filterOutResult(setFilter: [boolean, Set<string>], result: string) { protected filterOutResult(setFilter: [boolean, Set<string>], result: string) {
if (setFilter != null) { if (setFilter != null) {
result = result.trim().toLowerCase(); const cleanResult = result != null ? result.trim().toLowerCase() : '--';
const excluded = setFilter[0]; const excluded = setFilter[0];
const set = setFilter[1]; const set = setFilter[1];
if (excluded && set.has(result)) { if (excluded && set.has(cleanResult)) {
return true; return true;
} else if (!excluded && !set.has(result)) { } else if (!excluded && !set.has(cleanResult)) {
return true; return true;
} }
} }

View File

@@ -1,13 +1,8 @@
import { JWT } from 'google-auth-library'; import { JWT } from 'google-auth-library';
import { import {
admin_directory_v1,
google, google,
GoogleApis,
} from 'googleapis'; } from 'googleapis';
import {
Admin,
Schema$Group,
Schema$User,
} from 'googleapis/build/src/apis/admin/directory_v1';
import { DirectoryType } from '../enums/directoryType'; import { DirectoryType } from '../enums/directoryType';
@@ -25,7 +20,7 @@ import { LogService } from 'jslib/abstractions/log.service';
export class GSuiteDirectoryService extends BaseDirectoryService implements DirectoryService { export class GSuiteDirectoryService extends BaseDirectoryService implements DirectoryService {
private client: JWT; private client: JWT;
private service: Admin; private service: admin_directory_v1.Admin;
private authParams: any; private authParams: any;
private dirConfig: GSuiteConfiguration; private dirConfig: GSuiteConfiguration;
private syncConfig: SyncConfiguration; private syncConfig: SyncConfiguration;
@@ -33,7 +28,7 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements Dire
constructor(private configurationService: ConfigurationService, private logService: LogService, constructor(private configurationService: ConfigurationService, private logService: LogService,
private i18nService: I18nService) { private i18nService: I18nService) {
super(); super();
this.service = google.admin<Admin>('directory_v1'); this.service = google.admin('directory_v1');
} }
async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> { async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> {
@@ -72,52 +67,66 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements Dire
private async getUsers(): Promise<UserEntry[]> { private async getUsers(): Promise<UserEntry[]> {
const entries: UserEntry[] = []; const entries: UserEntry[] = [];
const query = this.createDirectoryQuery(this.syncConfig.userFilter); const query = this.createDirectoryQuery(this.syncConfig.userFilter);
let nextPageToken: string = null;
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);
}
const filter = this.createCustomSet(this.syncConfig.userFilter); const filter = this.createCustomSet(this.syncConfig.userFilter);
if (res.data.users != null) { while (true) {
for (const user of res.data.users) { this.logService.info('Querying users - nextPageToken:' + nextPageToken);
if (this.filterOutResult(filter, user.primaryEmail)) { const p = Object.assign({ query: query, pageToken: nextPageToken }, this.authParams);
continue; 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); nextPageToken = res.data.nextPageToken;
if (entry != null) { if (res.data.users != null) {
entries.push(entry); 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.'); nextPageToken = null;
p = Object.assign({ showDeleted: true, query: query }, this.authParams); while (true) {
const delRes = await this.service.users.list(p); this.logService.info('Querying deleted users - nextPageToken:' + nextPageToken);
if (delRes.status !== 200) { const p = Object.assign({ showDeleted: true, query: query, pageToken: nextPageToken }, this.authParams);
throw new Error('Deleted user list API failed: ' + delRes.statusText); 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) { nextPageToken = delRes.data.nextPageToken;
for (const user of delRes.data.users) { if (delRes.data.users != null) {
if (this.filterOutResult(filter, user.primaryEmail)) { for (const user of delRes.data.users) {
continue; 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 (nextPageToken == null) {
if (entry != null) { break;
entries.push(entry);
}
} }
} }
return entries; 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) { if ((user.emails == null || user.emails === '') && !deleted) {
return null; return null;
} }
@@ -125,7 +134,7 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements Dire
const entry = new UserEntry(); const entry = new UserEntry();
entry.referenceId = user.id; entry.referenceId = user.id;
entry.externalId = 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.disabled = user.suspended || false;
entry.deleted = deleted; entry.deleted = deleted;
return entry; return entry;
@@ -133,52 +142,75 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements Dire
private async getGroups(setFilter: [boolean, Set<string>]): Promise<GroupEntry[]> { private async getGroups(setFilter: [boolean, Set<string>]): Promise<GroupEntry[]> {
const entries: GroupEntry[] = []; const entries: GroupEntry[] = [];
let nextPageToken: string = null;
this.logService.info('Querying groups.'); while (true) {
const res = await this.service.groups.list(this.authParams); this.logService.info('Querying groups - nextPageToken:' + nextPageToken);
if (res.status !== 200) { const p = Object.assign({ pageToken: nextPageToken }, this.authParams);
throw new Error('Group list API failed: ' + res.statusText); const res = await this.service.groups.list(p);
} if (res.status !== 200) {
if (res.data.groups != null) { throw new Error('Group list API failed: ' + res.statusText);
for (const group of res.data.groups) { }
if (!this.filterOutResult(setFilter, group.name)) {
const entry = await this.buildGroup(group); nextPageToken = res.data.nextPageToken;
entries.push(entry); 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);
}
} }
} }
if (nextPageToken == null) {
break;
}
} }
return entries; return entries;
} }
private async buildGroup(group: Schema$Group) { private async buildGroup(group: admin_directory_v1.Schema$Group) {
let nextPageToken: string = null;
const entry = new GroupEntry(); const entry = new GroupEntry();
entry.referenceId = group.id; entry.referenceId = group.id;
entry.externalId = group.id; entry.externalId = group.id;
entry.name = group.name; entry.name = group.name;
const p = Object.assign({ groupKey: group.id }, this.authParams); while (true) {
const memRes = await this.service.members.list(p); const p = Object.assign({ groupKey: group.id, pageToken: nextPageToken }, this.authParams);
if (memRes.status !== 200) { const memRes = await this.service.members.list(p);
this.logService.warning('Group member list API failed: ' + memRes.statusText); if (memRes.status !== 200) {
return entry; this.logService.warning('Group member list API failed: ' + memRes.statusText);
} return entry;
}
if (memRes.data.members != null) { nextPageToken = memRes.data.nextPageToken;
for (const member of memRes.data.members) { if (memRes.data.members != null) {
if (member.role.toLowerCase() !== 'member') { for (const member of memRes.data.members) {
continue; if (member.type == null) {
} continue;
if (member.status.toLowerCase() !== 'active') { }
continue; if (member.role == null || member.role.toLowerCase() !== 'member') {
} continue;
}
if (member.status == null || member.status.toLowerCase() !== 'active') {
continue;
}
if (member.type.toLowerCase() === 'user') { const type = member.type.toLowerCase();
entry.userMemberExternalIds.add(member.id); if (type === 'user') {
} else if (member.type.toLowerCase() === 'group') { entry.userMemberExternalIds.add(member.id);
entry.groupMemberReferenceIds.add(member.id); } else if (type === 'group') {
entry.groupMemberReferenceIds.add(member.id);
}
} }
} }
if (nextPageToken == null) {
break;
}
} }
return entry; return entry;

View File

@@ -0,0 +1,25 @@
import {
deletePassword,
getPassword,
setPassword,
} from 'keytar';
import { StorageService } from 'jslib/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;
});
}
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);
}
}

View File

@@ -1,3 +1,4 @@
import * as fs from 'fs';
import * as ldap from 'ldapjs'; import * as ldap from 'ldapjs';
import { DirectoryType } from '../enums/directoryType'; import { DirectoryType } from '../enums/directoryType';
@@ -108,10 +109,14 @@ export class LdapDirectoryService implements DirectoryService {
this.syncConfig.emailPrefixAttribute != null && this.syncConfig.emailSuffix != null) { this.syncConfig.emailPrefixAttribute != null && this.syncConfig.emailSuffix != null) {
const prefixAttr = this.getAttr(searchEntry, this.syncConfig.emailPrefixAttribute); const prefixAttr = this.getAttr(searchEntry, this.syncConfig.emailPrefixAttribute);
if (prefixAttr != null) { 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() === '')) { if (!user.deleted && (user.email == null || user.email.trim() === '')) {
return null; return null;
} }
@@ -322,10 +327,32 @@ export class LdapDirectoryService implements DirectoryService {
const url = 'ldap' + (this.dirConfig.ssl ? 's' : '') + '://' + this.dirConfig.hostname + const url = 'ldap' + (this.dirConfig.ssl ? 's' : '') + '://' + this.dirConfig.hostname +
':' + this.dirConfig.port; ':' + this.dirConfig.port;
const options: ldap.ClientOptions = {
url: url.trim().toLowerCase(),
};
if (this.dirConfig.ssl) {
const tlsOptions: any = {};
if (this.dirConfig.sslAllowUnauthorized != null) {
tlsOptions.rejectUnauthorized = !this.dirConfig.sslAllowUnauthorized;
}
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);
}
if (Object.keys(tlsOptions).length > 0) {
options.tlsOptions = tlsOptions;
}
}
this.client = ldap.createClient({ this.client = ldap.createClient(options);
url: url.toLowerCase(),
});
const user = this.dirConfig.username == null || this.dirConfig.username.trim() === '' ? null : const user = this.dirConfig.username == null || this.dirConfig.username.trim() === '' ? null :
this.dirConfig.username; this.dirConfig.username;

View File

@@ -104,7 +104,7 @@ export class OktaDirectoryService extends BaseDirectoryService implements Direct
const entry = new UserEntry(); const entry = new UserEntry();
entry.externalId = user.id; entry.externalId = user.id;
entry.referenceId = 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.deleted = user.status === 'DEPROVISIONED';
entry.disabled = user.status === 'SUSPENDED'; entry.disabled = user.status === 'SUSPENDED';
return entry; return entry;

View File

@@ -13,7 +13,6 @@ import { CryptoFunctionService } from 'jslib/abstractions/cryptoFunction.service
import { I18nService } from 'jslib/abstractions/i18n.service'; import { I18nService } from 'jslib/abstractions/i18n.service';
import { LogService } from 'jslib/abstractions/log.service'; import { LogService } from 'jslib/abstractions/log.service';
import { MessagingService } from 'jslib/abstractions/messaging.service'; import { MessagingService } from 'jslib/abstractions/messaging.service';
import { StorageService } from 'jslib/abstractions/storage.service';
import { Utils } from 'jslib/misc/utils'; import { Utils } from 'jslib/misc/utils';
@@ -24,9 +23,6 @@ import { GSuiteDirectoryService } from './gsuite-directory.service';
import { LdapDirectoryService } from './ldap-directory.service'; import { LdapDirectoryService } from './ldap-directory.service';
import { OktaDirectoryService } from './okta-directory.service'; import { OktaDirectoryService } from './okta-directory.service';
const Keys = {
};
export class SyncService { export class SyncService {
private dirType: DirectoryType; private dirType: DirectoryType;
@@ -73,7 +69,7 @@ export class SyncService {
const reqJson = JSON.stringify(req); const reqJson = JSON.stringify(req);
let hash: string = null; let hash: string = null;
const hashBuf = await this.cryptoFunctionService.hash(this.apiService.baseUrl + reqJson, 'sha256'); const hashBuf = await this.cryptoFunctionService.hash(this.apiService.apiBaseUrl + reqJson, 'sha256');
if (hashBuf != null) { if (hashBuf != null) {
hash = Utils.fromBufferToB64(hashBuf); hash = Utils.fromBufferToB64(hashBuf);
} }
@@ -149,6 +145,9 @@ export class SyncService {
for (const u of users) { for (const u of users) {
const iu = new ImportDirectoryRequestUser(); const iu = new ImportDirectoryRequestUser();
iu.email = u.email; iu.email = u.email;
if (iu.email != null) {
iu.email = iu.email.trim().toLowerCase();
}
iu.externalId = u.externalId; iu.externalId = u.externalId;
iu.deleted = u.deleted || (removeDisabled && u.disabled); iu.deleted = u.deleted || (removeDisabled && u.disabled);
model.users.push(iu); model.users.push(iu);

100
src/utils.ts Normal file
View File

@@ -0,0 +1,100 @@
import { I18nService } from 'jslib/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);
});
}
}

View File

@@ -11,34 +11,54 @@
"types": [], "types": [],
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"tldjs": [
"jslib/src/misc/tldjs.noop"
],
"jslib/*": [ "jslib/*": [
"jslib/src/*" "jslib/src/*"
], ],
"@angular/*": [ "@angular/*": [
"node_modules/@angular/*" "node_modules/@angular/*"
], ],
"angular2-toaster": [
"node_modules/angular2-toaster"
],
"angulartics2": [
"node_modules/angulartics2"
],
"electron": [ "electron": [
"node_modules/electron" "node_modules/electron"
],
"node": [
"node_modules/@types/node"
],
"duo_web_sdk": [
"node_modules/duo_web_sdk"
] ]
} }
}, },
"angularCompilerOptions": {
"preserveWhitespaces": true
},
"exclude": [ "exclude": [
"node_modules", "node_modules",
"jslib/node_modules", "jslib/node_modules",
"jslib/src/services/index.ts",
"jslib/src/services/webCryptoFunction.service.ts", "jslib/src/services/webCryptoFunction.service.ts",
"jslib/src/services/search.service.ts", "jslib/src/services/search.service.ts",
"jslib/src/services/nodeApi.service.ts", "jslib/src/services/nodeApi.service.ts",
"jslib/src/services/lowdbStorage.service.ts",
"jslib/src/services/export.service.ts", "jslib/src/services/export.service.ts",
"jslib/src/services/notifications.service.ts",
"jslib/src/services/passwordGeneration.service.ts",
"jslib/src/abstractions/index.ts",
"jslib/src/abstractions/passwordGeneration.service.ts",
"jslib/src/angular/components/export.component.ts", "jslib/src/angular/components/export.component.ts",
"jslib/src/angular/components/register.component.ts",
"jslib/src/angular/components/password-generator.component.ts",
"jslib/src/angular/components/password-generator-history.component.ts",
"jslib/src/angular/pipes/color-password.pipe.ts",
"jslib/src/angular/directives/flex-copy.directive.ts",
"jslib/src/importers",
"dist", "dist",
"dist-cli",
"jslib/dist", "jslib/dist",
"build", "build",
"build-cli",
"jslib/spec" "jslib/spec"
] ]
} }

View File

@@ -48,6 +48,7 @@
"check-preblock", "check-preblock",
"check-separator", "check-separator",
"check-type" "check-type"
] ],
"max-classes-per-file": false
} }
} }

1
webfonts.list Normal file
View File

@@ -0,0 +1 @@
Open+Sans:300,300i,400,400i,600,600i,700,700i,800,800i&subset=cyrillic,cyrillic-ext,greek,greek-ext,latin-ext

80
webpack.cli.js Normal file
View File

@@ -0,0 +1,80 @@
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');
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([
path.resolve(__dirname, 'build-cli/*'),
]),
new CopyWebpackPlugin([
{ 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'],
alias: {
jslib: path.join(__dirname, 'jslib/src'),
tldjs: path.join(__dirname, 'jslib/src/misc/tldjs.noop'),
// ref: https://github.com/bitinn/node-fetch/issues/493
'node-fetch$': 'node-fetch/lib/index.js',
},
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;

View File

@@ -1,5 +1,4 @@
const path = require('path'); 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 CopyWebpackPlugin = require('copy-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin'); const CleanWebpackPlugin = require('clean-webpack-plugin');
@@ -11,48 +10,53 @@ const common = {
{ {
test: /\.ts$/, test: /\.ts$/,
enforce: 'pre', enforce: 'pre',
loader: 'tslint-loader' loader: 'tslint-loader',
}, },
{ {
test: /\.tsx?$/, test: /\.tsx?$/,
use: 'ts-loader', use: 'ts-loader',
exclude: /node_modules\/(?!(@bitwarden)\/).*/ exclude: /node_modules\/(?!(@bitwarden)\/).*/,
}, },
] ],
}, },
plugins: [], plugins: [],
resolve: { resolve: {
extensions: ['.tsx', '.ts', '.js'], extensions: ['.tsx', '.ts', '.js'],
alias: { alias: {
jslib: path.join(__dirname, 'jslib/src') jslib: path.join(__dirname, 'jslib/src'),
} tldjs: path.join(__dirname, 'jslib/src/misc/tldjs.noop'),
},
}, },
output: { output: {
filename: '[name].js', filename: '[name].js',
path: path.resolve(__dirname, 'build') path: path.resolve(__dirname, 'build'),
} },
}; };
const main = { const main = {
mode: 'production',
target: 'electron-main', target: 'electron-main',
node: { node: {
__dirname: false, __dirname: false,
__filename: false __filename: false,
}, },
entry: { entry: {
'main': './src/main.ts' 'main': './src/main.ts',
},
optimization: {
minimize: false,
}, },
module: { module: {
rules: [ rules: [
{ {
test: /\.node$/, test: /\.node$/,
loader: 'node-loader' loader: 'node-loader',
}, },
] ],
}, },
plugins: [ plugins: [
new CleanWebpackPlugin([ new CleanWebpackPlugin([
path.resolve(__dirname, 'build/*') path.resolve(__dirname, 'build/*'),
]), ]),
new CopyWebpackPlugin([ new CopyWebpackPlugin([
'./src/package.json', './src/package.json',
@@ -60,7 +64,7 @@ const main = {
{ from: './src/locales', to: 'locales' }, { from: './src/locales', to: 'locales' },
]), ]),
], ],
externals: [nodeExternals()] externals: [nodeExternals()],
}; };
module.exports = merge(common, main); module.exports = merge(common, main);

View File

@@ -2,21 +2,13 @@ const path = require('path');
const webpack = require('webpack'); const webpack = require('webpack');
const merge = require('webpack-merge'); const merge = require('webpack-merge');
const HtmlWebpackPlugin = require('html-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin');
const GoogleFontsPlugin = require("google-fonts-webpack-plugin");
const ExtractTextPlugin = require('extract-text-webpack-plugin'); const ExtractTextPlugin = require('extract-text-webpack-plugin');
const AngularCompilerPlugin = require('@ngtools/webpack').AngularCompilerPlugin; 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({ const extractCss = new ExtractTextPlugin({
filename: '[name].css', filename: '[name].css',
disable: false, disable: false,
allChunks: true allChunks: true,
}); });
const common = { const common = {
@@ -25,11 +17,11 @@ const common = {
{ {
test: /\.ts$/, test: /\.ts$/,
enforce: 'pre', enforce: 'pre',
loader: 'tslint-loader' loader: 'tslint-loader',
}, },
{ {
test: /(?:\.ngfactory\.js|\.ngstyle\.js|\.ts)$/, test: /(?:\.ngfactory\.js|\.ngstyle\.js|\.ts)$/,
loader: '@ngtools/webpack' loader: '@ngtools/webpack',
}, },
{ {
test: /\.(jpe?g|png|gif|svg)$/i, test: /\.(jpe?g|png|gif|svg)$/i,
@@ -39,39 +31,54 @@ const common = {
options: { options: {
name: '[name].[ext]', name: '[name].[ext]',
outputPath: 'images/', outputPath: 'images/',
} },
}] }],
} },
] ],
}, },
plugins: [], plugins: [],
resolve: { resolve: {
extensions: ['.tsx', '.ts', '.js', '.json'], extensions: ['.tsx', '.ts', '.js', '.json'],
alias: { alias: {
jslib: path.join(__dirname, 'jslib/src') jslib: path.join(__dirname, 'jslib/src'),
}, },
symlinks: false, symlinks: false,
modules: [path.resolve('node_modules')] modules: [path.resolve('node_modules')],
}, },
output: { output: {
filename: '[name].js', filename: '[name].js',
path: path.resolve(__dirname, 'build') path: path.resolve(__dirname, 'build'),
} },
}; };
const renderer = { const renderer = {
mode: 'production',
target: 'electron-renderer', target: 'electron-renderer',
node: { node: {
__dirname: false __dirname: false,
}, },
entry: { entry: {
'app/main': './src/app/main.ts' 'app/main': './src/app/main.ts',
},
optimization: {
minimize: false,
splitChunks: {
cacheGroups: {
commons: {
test: /[\\/]node_modules[\\/]/,
name: 'app/vendor',
chunks: (chunk) => {
return chunk.name === 'app/main';
},
},
},
},
}, },
module: { module: {
rules: [ rules: [
{ {
test: /\.(html)$/, test: /\.(html)$/,
loader: 'html-loader' loader: 'html-loader',
}, },
{ {
test: /.(ttf|otf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/, test: /.(ttf|otf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/,
@@ -80,9 +87,9 @@ const renderer = {
loader: 'file-loader', loader: 'file-loader',
options: { options: {
name: '[name].[ext]', name: '[name].[ext]',
outputPath: 'fonts/' outputPath: 'fonts/',
} },
}] }],
}, },
{ {
test: /\.scss$/, test: /\.scss$/,
@@ -93,40 +100,27 @@ const renderer = {
}, },
{ {
loader: 'sass-loader', loader: 'sass-loader',
} },
], ],
publicPath: '../' publicPath: '../',
}) })
}, },
] // Hide System.import warnings. ref: https://github.com/angular/angular/issues/21560
{
test: /[\/\\]@angular[\/\\].+\.js$/,
parser: { system: true },
},
],
}, },
plugins: [ 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({ new AngularCompilerPlugin({
tsConfigPath: 'tsconfig.json', tsConfigPath: 'tsconfig.json',
entryModule: 'src/app/app.module#AppModule', entryModule: 'src/app/app.module#AppModule',
sourceMap: true sourceMap: true,
}), }),
// ref: https://github.com/angular/angular/issues/20357 // ref: https://github.com/angular/angular/issues/20357
new webpack.ContextReplacementPlugin(/\@angular(\\|\/)core(\\|\/)esm5/, new webpack.ContextReplacementPlugin(/\@angular(\\|\/)core(\\|\/)fesm5/,
path.resolve(__dirname, './src')), path.resolve(__dirname, './src')),
new webpack.optimize.CommonsChunkPlugin({
name: 'app/vendor',
chunks: ['app/main'],
minChunks: isVendorModule
}),
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
template: './src/index.html', template: './src/index.html',
filename: 'index.html', filename: 'index.html',
@@ -137,8 +131,8 @@ const renderer = {
include: ['app/main.js'] include: ['app/main.js']
}), }),
new webpack.DefinePlugin({ 'global.GENTLY': false }), new webpack.DefinePlugin({ 'global.GENTLY': false }),
extractCss extractCss,
] ],
}; };
module.exports = merge(common, renderer); module.exports = merge(common, renderer);