1
0
mirror of https://github.com/bitwarden/directory-connector synced 2025-12-15 07:43:27 +00:00

Compare commits

..

102 Commits

Author SHA1 Message Date
Kyle Spearrin
19e1049566 use env variable for apple id password 2020-12-03 23:10:30 -05:00
Kyle Spearrin
87bdc88e22 bump version 2020-12-03 23:01:06 -05:00
Chad Scharf
950e3ae91e Merge pull request #78 from bitwarden/update-jslib-sso
jslib update to fix SSO from DC
2020-12-02 14:52:53 -05:00
Chad Scharf
a37532e1ad Merge branch 'master' into update-jslib-sso
merge lastest from master into jslib update branch
2020-12-02 14:27:55 -05:00
Chad Scharf
21ff5f311b Merge pull request #77 from bitwarden/update-jslib
Update jslib
2020-12-02 14:24:30 -05:00
Chad Scharf
7dd12cf0cb jslib update to fix SSO from DC 2020-12-02 14:16:30 -05:00
Chad Scharf
6f8df7a690 Update jslib 2020-11-23 16:08:12 -05:00
Chad Scharf
1ff0ef1f90 Merge pull request #75 from bitwarden/fix-build-error
Revert https-proxy-agent version
2020-11-13 15:07:31 -05:00
Chad Scharf
196fc10d80 Revert https-proxy-agent version 2020-11-13 14:54:36 -05:00
Chad Scharf
7496de4cc6 Merge pull request #74 from kitos9112/master
Bump https-proxy-agent dep. to 5.0.0
2020-11-13 14:09:02 -05:00
Marcos Soutullo Rodriguez
02a6adf6a2 Bump https-proxy-agent dep. to 5.0.0 2020-11-13 18:51:54 +00:00
Chad Scharf
6c95575a8f Merge pull request #73 from bitwarden/version-jslib-update
Version bump + jslib update
2020-10-21 09:46:19 -04:00
Chad Scharf
39755e89a8 Version bump + jslib update 2020-10-21 09:39:54 -04:00
Chad Scharf
c5d3ca218e Merge pull request #71 from bitwarden/fix/bwdc-lint-errors
Fix lint errors/warnings
2020-10-20 10:22:49 -04:00
Chad Scharf
87a2a2a0e4 Merge pull request #72 from bitwarden/add-logging-to-lowdb-storage-svc
Add logger to lowdb storage service
2020-10-20 09:33:38 -04:00
Chad Scharf
5904da2eb1 Add logger to lowdb storage service 2020-10-19 16:05:15 -04:00
Chad Scharf
1ac0c81661 Fix lint errors/warnings 2020-10-19 13:50:08 -04:00
Vincent Salucci
955711714d [SSO] New user provision flow jslib update (5e0a2d1 -> d84d6da) (#70)
* Updated import/constructor

* Update jslib (5e0a2d1 -> d84d6da)
2020-10-14 08:59:40 -05:00
Kyle Spearrin
38758caac4 update jslib 2020-09-23 12:02:31 -04:00
Kyle Spearrin
b41a1bdbf4 bump version 2020-09-15 16:38:50 -04:00
Chad Scharf
a400ab7f7d update jslib (#67) 2020-09-15 11:00:20 -04:00
Vincent Salucci
e5d0405882 Added await to prevent race condition for successRoute (#66) 2020-09-10 12:35:52 -05:00
Vincent Salucci
f2137c02f7 Adjusted new success route (#65) 2020-09-09 09:22:54 -05:00
Kyle Spearrin
4c61f465a3 update jslib 2020-09-08 11:24:49 -04:00
Kyle Spearrin
6d22041eab update jslib 2020-09-03 10:35:55 -04:00
Vincent Salucci
752e26db6d [SSO] Adjust set password flow (#64)
* Removed set-password // Adjusted sso UI // Added string

* Removed test logic

* Update jslib (6ab444a -> 700e945)

* Passing null for expectes sync service

* Removing unnecessary async

* Deleted set-password files. RIP

* Removed non-existent import (fixes build)
2020-08-28 09:54:45 -05:00
Vincent Salucci
094ec23f04 Merge pull request #62 from bitwarden/m-set-password
[SSO] Added set password flow
2020-08-24 13:10:30 -05:00
Vincent Salucci
af8ff2901e Fixed theme colors // fixed formatting issues in misc 2020-08-24 11:50:25 -05:00
Vincent Salucci
628689c990 Removed notorization shortcut // Cleaned up extra comma in the service module 2020-08-22 14:43:40 -05:00
Vincent Salucci
ab37221182 Updated UI for password strength // Updated success route // Add more missing strings 2020-08-22 14:28:22 -05:00
Vincent Salucci
626892473f Updated dependency chain // Updated UI // Added Policy Service // Added missing strings // Added callout styles 2020-08-22 12:47:50 -05:00
Vincent Salucci
c621677852 update jslib (5d874d0 -> 6ab444a) 2020-08-22 08:06:08 -05:00
Vincent Salucci
6650b4848d Initial commit for bootstrap styled set-password 2020-08-21 09:24:09 -05:00
Vincent Salucci
c07c56f89b Initial commit of set password flow 2020-08-20 21:32:20 -05:00
Oscar Hinton
294590882f Upgrade to Angular 9 (#60) 2020-08-19 15:49:56 -04:00
Kyle Spearrin
9151b9c2d6 support launching sso from login page button (#59) 2020-08-12 16:03:41 -04:00
Kyle Spearrin
5817468d09 fix login ctor 2020-08-11 14:11:47 -04:00
Kyle Spearrin
31dd20999c update jslib 2020-08-11 12:07:55 -04:00
A Codeweavers Infrastructure Bod
4eb9c9bd4d Fix for #55 (#56)
* Groups that reference all users in an organisation were not being populated

They now are, based on the "customer" member type. The null check for member.status is now not required as the property was only null for groups and now that comparison will not occur.

* If the user is not configured to sync users, but is syncing groups this errored with "users is not iterable"

* Update gsuite-directory.service.ts

Co-authored-by: Kyle Spearrin <kspearrin@users.noreply.github.com>
2020-08-10 09:39:00 -04:00
Kyle Spearrin
150164534f sso login from bwdc desktop app (#58) 2020-08-05 11:09:06 -04:00
Kyle Spearrin
2b2d8a9fab CLI support for SSO Login (#57)
* sso login support

* fix build and lint issues

* allow web vault URL to be set
2020-08-04 14:19:53 -04:00
Kyle Spearrin
fb122cbbdb fix okta paging (#51)
* fix okta paging

* remove okta package

* use node https instead of a library

* remove bent types

* add 500ms throttle to avoid rate limiter
2020-07-02 14:50:54 -04:00
Kyle Spearrin
0b37857d29 formatting fix 2020-07-02 13:32:08 -04:00
Kyle Spearrin
15c1876687 dont skip disbaled users if not removeDisabled (#50) 2020-07-01 16:15:32 -04:00
Kyle Spearrin
473a6e391d update jslib 2020-06-26 15:23:06 -04:00
Kyle Spearrin
13ad64e6f3 if email field is not valid, try username 2020-06-22 10:43:55 -04:00
Kyle Spearrin
04e278249e add support for email coming from username prop (#47)
* add support for email coming from username prop

* feedback adjustments
2020-06-22 10:28:06 -04:00
Kyle Spearrin
e12c4ea1e2 include all members of the group (#46) 2020-06-16 14:42:38 -04:00
Kyle Spearrin
acfa8632af ignore case 2020-06-04 09:59:45 -04:00
Kyle Spearrin
f53c1f5605 update jslib 2020-05-29 11:10:14 -04:00
Kyle Spearrin
f0f7f89ea8 null checks 2020-05-22 14:28:20 -04:00
Kyle Spearrin
059ff0647a Added option to let user control the paged param (#45) 2020-05-19 12:16:30 -04:00
Kyle Spearrin
d94e5b0620 debug paged = false 2020-05-19 10:27:40 -04:00
Kyle Spearrin
71cd11eedf update webpack to use MiniCssExtractPlugin 2020-05-08 12:18:03 -04:00
Kyle Spearrin
ecea04bc08 brand color updates 2020-05-05 16:59:26 -04:00
Kyle Spearrin
0575ee5507 skip null user emails 2020-05-04 08:35:39 -04:00
Kyle Spearrin
8b2fa8405b add paging to group members api request 2020-04-24 11:25:00 -04:00
Kyle Spearrin
7d36a50687 update lunr types (#37) 2020-04-14 15:55:39 -04:00
Kyle Spearrin
f7dd9d8d5b filter unsupported user entries by email length 2020-03-27 10:36:41 -04:00
Kyle Spearrin
379b4f4612 update jslib 2020-03-21 00:20:41 -04:00
Kyle Spearrin
4652668e1b Enable OneLogin type from settings 2020-03-13 23:16:19 -04:00
Kyle Spearrin
4e02a8571e UserSelectParams for Azure AD 2020-03-13 23:15:39 -04:00
Kyle Spearrin
bc927a65ac if no dc= in rootPath, just return pathPrefix 2020-03-13 10:12:17 -04:00
Kyle Spearrin
90f1a1c115 noiteration option 2020-03-13 09:06:01 -04:00
Kyle Spearrin
c48acf6038 support onelogin in cli 2020-03-13 09:04:40 -04:00
Kyle Spearrin
2640e8c890 onelogin directory implementation 2020-03-12 22:25:38 -04:00
Kyle Spearrin
094ec55e7f update jslib 2020-03-12 20:27:44 -04:00
Kyle Spearrin
fe39bdac42 remove request interceptor 2020-03-12 14:44:09 -04:00
Kyle Spearrin
2c98d50c43 remove old client ref 2020-03-12 12:53:26 -04:00
Kyle Spearrin
d374cff51c stub out OneLogin connector 2020-03-12 12:42:04 -04:00
Kyle Spearrin
d9e7256804 upgrade electron builder 2020-03-06 11:20:22 -05:00
Kyle Spearrin
0ef2d1523e Revert "update electron builder/updater"
This reverts commit 05bad6f671.
2020-03-06 11:19:13 -05:00
Kyle Spearrin
05bad6f671 update electron builder/updater 2020-03-06 09:24:27 -05:00
Kyle Spearrin
5a62cfcda1 4.2 updater 2020-03-05 10:43:31 -05:00
Kyle Spearrin
634d38510d bump version 2020-03-05 09:32:05 -05:00
Kyle Spearrin
bf27872973 update jslib. tweaks to start tls 2020-03-04 11:31:35 -05:00
Colin Campbell
20bb5a4926 starttls: Support LDAP STARTTLS (#33)
* starttls: Support LDAP STARTTLS

* starttls: Re-roll to preserve old config
2020-03-04 11:08:47 -05:00
Kyle Spearrin
f63fb3ffa0 bitwarden inc. 2020-02-18 22:36:38 -05:00
Kyle Spearrin
f11c32c606 update https proxy 2020-01-27 14:00:29 -05:00
Kyle Spearrin
0b4e22a952 symlink jslib on mac/lin 2020-01-27 12:46:18 -05:00
Kyle Spearrin
a85dbff3a5 upgrade to electron 6 2020-01-27 10:00:21 -05:00
Kyle Spearrin
d2835dd577 remove angualr http and upgrade node-sass 2020-01-27 09:03:41 -05:00
Kyle Spearrin
62abc99b61 devtool false 2020-01-22 14:22:32 -05:00
Kyle Spearrin
20de62cc79 update jslib 2020-01-09 17:30:32 -05:00
Kyle Spearrin
731614279f npm i 2019-10-21 08:45:25 -04:00
Kyle Spearrin
2be751dc8e npm audit fix 2019-10-07 14:57:35 -04:00
Kyle Spearrin
e0409941a3 symlink 2019-09-25 16:10:10 -04:00
Kyle Spearrin
e6a5a3c8c1 update jslib 2019-08-30 14:21:52 -04:00
Kyle Spearrin
3f3590a223 update jslib 2019-08-20 13:47:52 -04:00
Kyle Spearrin
c1f64d7b82 disable-library-validation entitlement 2019-07-31 23:44:11 -04:00
Kyle Spearrin
f90611c96f update keytar 2019-07-31 23:43:34 -04:00
Kyle Spearrin
630e21f7c1 update jslib 2019-07-25 20:43:09 -04:00
Kyle Spearrin
2e81642c0e bump version 2019-07-25 14:21:04 -04:00
Kyle Spearrin
39514b9550 update jslib 2019-07-25 14:18:23 -04:00
Kyle Spearrin
2da82d5610 notarize directory connector 2019-07-25 14:17:50 -04:00
Kyle Spearrin
69f33a08b6 upgrade electron builder 2019-07-24 15:30:43 -04:00
Kyle Spearrin
a05e9c7746 upgrade to electron 5 2019-07-24 14:37:43 -04:00
Kyle Spearrin
d8031e4f49 update jslib 2019-07-10 08:39:21 -04:00
Kyle Spearrin
e6aa07ba5c plaintext secrets env variable 2019-07-05 11:57:25 -04:00
Kyle Spearrin
173129014a re-set state service on log in 2019-07-02 08:37:52 -04:00
Kyle Spearrin
8d4baa6d31 simlink for windows 2019-06-24 21:13:07 -04:00
Kyle Spearrin
20463ce653 update jslib, add node https proxy 2019-06-24 11:09:41 -04:00
40 changed files with 4877 additions and 4334 deletions

1
.gitignore vendored
View File

@@ -14,3 +14,4 @@ yarn-error.log
.DS_Store
*.nupkg
*.provisionprofile
*.env

2
jslib

Submodule jslib updated: 8375f7381a...abb54f0073

View File

@@ -12,14 +12,14 @@ BLOCK "StringFileInfo"
{
BLOCK "040904b0"
{
VALUE "CompanyName", "8bit Solutions LLC"
VALUE "CompanyName", "Bitwarden Inc."
VALUE "ProductName", "Bitwarden"
VALUE "FileDescription", "Bitwarden Directory Connector CLI"
VALUE "FileVersion", "$env:PACKAGE_VERSION"
VALUE "ProductVersion", "$env:PACKAGE_VERSION"
VALUE "OriginalFilename", "bwdc.exe"
VALUE "InternalName", "bwdc"
VALUE "LegalCopyright", "Copyright 8bit Solutions LLC"
VALUE "LegalCopyright", "Copyright Bitwarden Inc."
}
}

7742
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,7 @@
"vault",
"password manager"
],
"author": "8bit Solutions LLC <hello@bitwarden.com> (https://bitwarden.com)",
"author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)",
"homepage": "https://bitwarden.com",
"repository": {
"type": "git",
@@ -22,6 +22,9 @@
"sub:pull": "git submodule foreach git pull origin master",
"sub:commit": "npm run sub:pull && git commit -am \"update submodule\"",
"postinstall": "npm run sub:init",
"symlink:win": "rm -rf ./jslib && cmd /c mklink /J .\\jslib ..\\jslib",
"symlink:mac": "npm run symlink:lin",
"symlink:lin": "rm -rf ./jslib && ln -s ../jslib ./jslib",
"rebuild": "./node_modules/.bin/electron-rebuild",
"reset": "rimraf ./node_modules/keytar/* && npm install",
"lint": "tslint src/**/*.ts || true",
@@ -38,10 +41,10 @@
"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:cli": "rimraf ./dist-cli/*",
"pack:lin": "npm run clean:dist && build --linux --x64 -p never",
"pack:mac": "npm run clean:dist && build --mac -p never",
"pack:win": "npm run clean:dist && build --win --x64 --ia32 -p never -c.win.certificateSubjectName=\"8bit Solutions LLC\"",
"pack:win:ci": "npm run clean:dist && build --win --x64 --ia32 -p never",
"pack:lin": "npm run clean:dist && electron-builder --linux --x64 -p never",
"pack:mac": "npm run clean:dist && electron-builder --mac -p never",
"pack:win": "npm run clean:dist && electron-builder --win --x64 --ia32 -p never -c.win.certificateSubjectName=\"8bit Solutions LLC\"",
"pack:win:ci": "npm run clean:dist && electron-builder --win --x64 --ia32 -p never",
"pack:cli": "npm run pack:cli:win | npm run pack:cli:mac | npm run pack:cli:lin",
"pack:cli:win": "pkg . --targets win-x64 --output ./dist-cli/windows/bwdc.exe",
"pack:cli:mac": "pkg . --targets macos-x64 --output ./dist-cli/macos/bwdc",
@@ -54,20 +57,25 @@
"dist:cli:win": "npm run build:cli:prod && npm run clean:dist:cli && npm run pack:cli:win",
"dist:cli:mac": "npm run build:cli:prod && npm run clean:dist:cli && npm run pack:cli:mac",
"dist:cli:lin": "npm run build:cli:prod && npm run clean:dist:cli && npm run pack:cli:lin",
"publish:lin": "npm run build:dist && npm run clean:dist && 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\""
"publish:lin": "npm run build:dist && npm run clean:dist && electron-builder --linux --x64 -p always",
"publish:mac": "npm run build:dist && npm run clean:dist && electron-builder --mac -p always",
"publish:win": "npm run build:dist && npm run clean:dist && electron-builder --win --x64 --ia32 -p always -c.win.certificateSubjectName=\"8bit Solutions LLC\""
},
"build": {
"appId": "com.bitwarden.directory-connector",
"copyright": "Copyright © 2015-2018 8bit Solutions LLC",
"copyright": "Copyright © 2015-2020 Bitwarden Inc.",
"directories": {
"buildResources": "resources",
"output": "dist",
"app": "build"
},
"afterSign": "scripts/notarize.js",
"mac": {
"category": "public.app-category.productivity",
"gatekeeperAssess": false,
"hardenedRuntime": true,
"entitlements": "resources/entitlements.mac.plist",
"entitlementsInherit": "resources/entitlements.mac.plist",
"target": [
"dmg",
"zip"
@@ -129,16 +137,16 @@
"assets": "./build-cli/**/*"
},
"devDependencies": {
"@angular/compiler-cli": "^7.2.11",
"@angular/compiler-cli": "^9.1.12",
"@microsoft/microsoft-graph-types": "^1.4.0",
"@ngtools/webpack": "^7.2.2",
"@ngtools/webpack": "^9.1.12",
"@types/commander": "^2.12.2",
"@types/form-data": "^2.2.1",
"@types/inquirer": "^0.0.43",
"@types/ldapjs": "^1.0.3",
"@types/lowdb": "^1.0.5",
"@types/lunr": "^2.1.6",
"@types/node": "^10.9.4",
"@types/lunr": "^2.3.3",
"@types/node": "^10.17.28",
"@types/node-fetch": "^2.1.2",
"@types/node-forge": "^0.7.5",
"@types/papaparse": "^4.5.3",
@@ -153,47 +161,46 @@
"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",
"electron": "6.1.7",
"electron-builder": "22.4.0",
"electron-notarize": "^0.2.1",
"electron-rebuild": "^1.9.0",
"electron-reload": "^1.5.0",
"mini-css-extract-plugin": "^0.9.0",
"file-loader": "^2.0.0",
"font-awesome": "4.7.0",
"gulp": "^4.0.0",
"gulp-google-webfonts": "^2.0.0",
"html-loader": "^0.5.5",
"html-webpack-plugin": "^3.2.0",
"node-abi": "^2.5.1",
"node-abi": "^2.9.0",
"node-loader": "^0.6.0",
"node-sass": "^4.11.0",
"node-sass": "^4.13.1",
"pkg": "4.3.4",
"rimraf": "^2.6.2",
"sass-loader": "^7.1.0",
"ts-loader": "^5.3.3",
"ts-loader": "^7.0.5",
"tslint": "^5.12.1",
"tslint-loader": "^3.5.4",
"typescript": "3.2.4",
"typescript": "3.8.3",
"webpack": "^4.29.0",
"webpack-cli": "^3.2.1",
"webpack-merge": "^4.2.1",
"webpack-node-externals": "^1.7.2"
},
"dependencies": {
"@angular/animations": "7.2.1",
"@angular/common": "7.2.1",
"@angular/compiler": "7.2.1",
"@angular/core": "7.2.1",
"@angular/forms": "7.2.1",
"@angular/http": "7.2.1",
"@angular/platform-browser": "7.2.1",
"@angular/platform-browser-dynamic": "7.2.1",
"@angular/router": "7.2.1",
"@angular/upgrade": "7.2.1",
"@angular/animations": "9.1.12",
"@angular/common": "9.1.12",
"@angular/compiler": "9.1.12",
"@angular/core": "9.1.12",
"@angular/forms": "9.1.12",
"@angular/platform-browser": "9.1.12",
"@angular/platform-browser-dynamic": "9.1.12",
"@angular/router": "9.1.12",
"@angular/upgrade": "9.1.12",
"@microsoft/microsoft-graph-client": "1.2.0",
"@okta/okta-sdk-nodejs": "1.2.0",
"angular2-toaster": "6.1.0",
"angulartics2": "6.3.0",
"angular2-toaster": "8.0.0",
"angulartics2": "9.1.0",
"big-integer": "1.6.36",
"bootstrap": "4.3.1",
"chalk": "2.4.1",
@@ -202,17 +209,21 @@
"duo_web_sdk": "git+https://github.com/duosecurity/duo_web_sdk.git",
"electron-log": "2.2.17",
"electron-store": "1.3.0",
"electron-updater": "4.0.6",
"electron-updater": "4.2.0",
"form-data": "2.3.2",
"googleapis": "33.0.0",
"googleapis": "43.0.0",
"https-proxy-agent": "4.0.0",
"inquirer": "6.2.0",
"keytar": "4.4.1",
"keytar": "4.13.0",
"ldapjs": "git+https://git@github.com/kspearrin/node-ldapjs.git",
"lowdb": "1.0.0",
"lunr": "2.3.3",
"node-fetch": "2.2.0",
"node-forge": "0.7.6",
"rxjs": "6.3.3",
"zone.js": "0.8.28"
"open": "7.1.0",
"rxjs": "6.6.2",
"tslib": "^2.0.1",
"zone.js": "0.10.3",
"zxcvbn": "4.4.2"
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
</dict>
</plist>

18
scripts/notarize.js Normal file
View File

@@ -0,0 +1,18 @@
require('dotenv').config();
const { notarize } = require('electron-notarize');
exports.default = async function notarizing(context) {
const { electronPlatformName, appOutDir } = context;
if (electronPlatformName !== 'darwin') {
return;
}
const appleId = process.env.APPLE_ID_USERNAME || process.env.APPLEID;
const appleIdPassword = process.env.APPLE_ID_PASSWORD || `@keychain:AC_PASSWORD`;
const appName = context.packager.appInfo.productFilename;
return await notarize({
appBundleId: 'com.bitwarden.directory-connector',
appPath: `${appOutDir}/${appName}.app`,
appleId: appleId,
appleIdPassword: appleIdPassword,
});
};

View File

@@ -17,6 +17,11 @@
</div>
<h4>{{'customEnvironment' | i18n}}</h4>
<p>{{'customEnvironmentFooter' | i18n}}</p>
<div class="form-group">
<label for="webVaultUrl">{{'webVaultUrl' | i18n}}</label>
<input id="webVaultUrl" type="text" name="WebVaultUrl" [(ngModel)]="webVaultUrl"
class="form-control">
</div>
<div class="form-group">
<label for="apiUrl">{{'apiUrl' | i18n}}</label>
<input id="apiUrl" type="text" name="ApiUrl" [(ngModel)]="apiUrl" class="form-control">

View File

@@ -18,18 +18,26 @@
[(ngModel)]="masterPassword" class="form-control">
</div>
</div>
<div class="d-flex">
<div>
<button type="submit" class="btn btn-primary" [disabled]="form.loading">
<i class="fa fa-spinner fa-fw fa-spin" [hidden]="!form.loading"></i>
<i class="fa fa-sign-in fa-fw" [hidden]="form.loading"></i>
{{'logIn' | i18n}}
</button>
<button type="button" class="btn btn-link" (click)="settings()">
<button type="button" class="btn btn-secondary ml-1" (click)="sso()">
<i class="fa fa-bank" aria-hidden="true"></i>
{{'enterpriseSingleSignOn' | i18n}}
</button>
</div>
<button type="button" class="btn btn-link ml-auto" (click)="settings()">
{{'settings' | i18n}}
</button>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
<ng-template #environment></ng-template>

View File

@@ -9,8 +9,12 @@ import { Router } from '@angular/router';
import { EnvironmentComponent } from './environment.component';
import { AuthService } from 'jslib/abstractions/auth.service';
import { CryptoFunctionService } from 'jslib/abstractions/cryptoFunction.service';
import { EnvironmentService } from 'jslib/abstractions/environment.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { StateService } from 'jslib/abstractions/state.service';
import { StorageService } from 'jslib/abstractions/storage.service';
import { LoginComponent as BaseLoginComponent } from 'jslib/angular/components/login.component';
@@ -21,12 +25,18 @@ import { ModalComponent } from 'jslib/angular/components/modal.component';
templateUrl: 'login.component.html',
})
export class LoginComponent extends BaseLoginComponent {
@ViewChild('environment', { read: ViewContainerRef }) environmentModal: ViewContainerRef;
@ViewChild('environment', { read: ViewContainerRef, static: true }) environmentModal: ViewContainerRef;
constructor(authService: AuthService, router: Router,
i18nService: I18nService, private componentFactoryResolver: ComponentFactoryResolver,
storageService: StorageService, platformUtilsService: PlatformUtilsService) {
super(authService, router, platformUtilsService, i18nService, storageService);
storageService: StorageService, stateService: StateService,
platformUtilsService: PlatformUtilsService, environmentService: EnvironmentService,
passwordGenerationService: PasswordGenerationService, cryptoFunctionService: CryptoFunctionService) {
super(authService, router,
platformUtilsService, i18nService,
stateService, environmentService,
passwordGenerationService, cryptoFunctionService,
storageService);
super.successRoute = '/tabs/dashboard';
}
@@ -40,4 +50,8 @@ export class LoginComponent extends BaseLoginComponent {
modal.close();
});
}
sso() {
return super.launchSsoBrowser('connector', 'bwdc://sso-callback');
}
}

View File

@@ -0,0 +1,30 @@
<div class="container-fluid">
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<div class="row justify-content-center">
<div class="col-md-8 col-lg-6">
<div class="card" *ngIf="!showMasterPassRedirect">
<div class="card-body">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
{{'loading' | i18n}}
</div>
</div>
<div class="card" *ngIf="showMasterPassRedirect">
<h5 class="card-header">{{'setMasterPassword' | i18n}}</h5>
<div class="card-body">
<p class="text-center">{{'setMasterPasswordRedirect' | i18n}}</p>
<hr>
<div class="d-flex">
<button type="button" class="btn btn-primary btn-block btn-submit"
(click)="launchWebVault()">
{{'launchWebVault' | i18n}}
</button>
<button type="button" class="btn btn-secondary btn-block ml-2 mt-0" (click)="logOut()">
{{'logOut' | i18n}}
</button>
</div>
</div>
</div>
</div>
</div>
</form>
</div>

View File

@@ -0,0 +1,61 @@
import { Component } from '@angular/core';
import {
ActivatedRoute,
Router,
} from '@angular/router';
import { ApiService } from 'jslib/abstractions/api.service';
import { AuthService } from 'jslib/abstractions/auth.service';
import { CryptoFunctionService } from 'jslib/abstractions/cryptoFunction.service';
import { EnvironmentService } from 'jslib/abstractions/environment.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { MessagingService } from 'jslib/abstractions/messaging.service';
import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { StateService } from 'jslib/abstractions/state.service';
import { StorageService } from 'jslib/abstractions/storage.service';
import { SsoComponent as BaseSsoComponent } from 'jslib/angular/components/sso.component';
@Component({
selector: 'app-sso',
templateUrl: 'sso.component.html',
})
export class SsoComponent extends BaseSsoComponent {
showMasterPassRedirect: boolean = false;
constructor(authService: AuthService, router: Router,
i18nService: I18nService, route: ActivatedRoute,
storageService: StorageService, stateService: StateService,
platformUtilsService: PlatformUtilsService, apiService: ApiService,
cryptoFunctionService: CryptoFunctionService,
passwordGenerationService: PasswordGenerationService, private messagingService: MessagingService,
private environmentService: EnvironmentService) {
super(authService, router, i18nService, route, storageService, stateService, platformUtilsService,
apiService, cryptoFunctionService, passwordGenerationService);
this.successRoute = '/tabs/dashboard';
this.redirectUri = 'bwdc://sso-callback';
this.clientId = 'connector';
this.onSuccessfulLoginChangePasswordNavigate = this.redirectSetMasterPass;
}
async redirectSetMasterPass() {
this.showMasterPassRedirect = true;
}
launchWebVault() {
const webUrl = this.environmentService.webVaultUrl == null ? 'https://vault.bitwarden.com' :
this.environmentService.webVaultUrl;
this.platformUtilsService.launchUri(webUrl);
}
async logOut() {
const confirmed = await this.platformUtilsService.showDialog(this.i18nService.t('logOutConfirmation'),
this.i18nService.t('logOut'), this.i18nService.t('logOut'), this.i18nService.t('cancel'));
if (confirmed) {
this.messagingService.send('logout');
}
}
}

View File

@@ -5,7 +5,10 @@ import {
ViewContainerRef,
} from '@angular/core';
import { Router } from '@angular/router';
import {
ActivatedRoute,
Router,
} from '@angular/router';
import { TwoFactorOptionsComponent } from './two-factor-options.component';
@@ -16,6 +19,8 @@ import { AuthService } from 'jslib/abstractions/auth.service';
import { EnvironmentService } from 'jslib/abstractions/environment.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { StateService } from 'jslib/abstractions/state.service';
import { StorageService } from 'jslib/abstractions/storage.service';
import { ModalComponent } from 'jslib/angular/components/modal.component';
import { TwoFactorComponent as BaseTwoFactorComponent } from 'jslib/angular/components/two-factor.component';
@@ -25,13 +30,19 @@ import { TwoFactorComponent as BaseTwoFactorComponent } from 'jslib/angular/comp
templateUrl: 'two-factor.component.html',
})
export class TwoFactorComponent extends BaseTwoFactorComponent {
@ViewChild('twoFactorOptions', { read: ViewContainerRef }) twoFactorOptionsModal: ViewContainerRef;
@ViewChild('twoFactorOptions', { read: ViewContainerRef, static: true }) twoFactorOptionsModal: ViewContainerRef;
constructor(authService: AuthService, router: Router,
i18nService: I18nService, apiService: ApiService,
platformUtilsService: PlatformUtilsService, environmentService: EnvironmentService,
private componentFactoryResolver: ComponentFactoryResolver) {
super(authService, router, i18nService, apiService, platformUtilsService, window, environmentService);
private componentFactoryResolver: ComponentFactoryResolver, stateService: StateService,
storageService: StorageService, route: ActivatedRoute) {
super(authService, router, i18nService, apiService, platformUtilsService, window, environmentService,
stateService, storageService, route);
}
async ngOnInit() {
await super.ngOnInit();
super.successRoute = '/tabs/dashboard';
}

View File

@@ -8,6 +8,7 @@ import { AuthGuardService } from './services/auth-guard.service';
import { LaunchGuardService } from './services/launch-guard.service';
import { LoginComponent } from './accounts/login.component';
import { SsoComponent } from './accounts/sso.component';
import { TwoFactorComponent } from './accounts/two-factor.component';
import { DashboardComponent } from './tabs/dashboard.component';
import { MoreComponent } from './tabs/more.component';
@@ -22,6 +23,7 @@ const routes: Routes = [
canActivate: [LaunchGuardService],
},
{ path: '2fa', component: TwoFactorComponent },
{ path: 'sso', component: SsoComponent },
{
path: 'tabs',
component: TabsComponent,

View File

@@ -48,7 +48,7 @@ const BroadcasterSubscriptionId = 'AppComponent';
<router-outlet></router-outlet>`,
})
export class AppComponent implements OnInit {
@ViewChild('settings', { read: ViewContainerRef }) settingsRef: ViewContainerRef;
@ViewChild('settings', { read: ViewContainerRef, static: true }) settingsRef: ViewContainerRef;
toasterConfig: ToasterConfig = new ToasterConfig({
showCloseButton: true,
@@ -133,6 +133,9 @@ export class AppComponent implements OnInit {
properties: { label: message.label },
});
break;
case 'ssoCallback':
this.router.navigate(['sso'], { queryParams: { code: message.code, state: message.state } });
break;
default:
}
});

View File

@@ -15,11 +15,13 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { AppComponent } from './app.component';
import { CalloutComponent } from 'jslib/angular/components/callout.component';
import { IconComponent } from 'jslib/angular/components/icon.component';
import { ModalComponent } from 'jslib/angular/components/modal.component';
import { EnvironmentComponent } from './accounts/environment.component';
import { LoginComponent } from './accounts/login.component';
import { SsoComponent } from './accounts/sso.component';
import { TwoFactorOptionsComponent } from './accounts/two-factor-options.component';
import { TwoFactorComponent } from './accounts/two-factor.component';
import { DashboardComponent } from './tabs/dashboard.component';
@@ -46,7 +48,7 @@ import { SearchCiphersPipe } from 'jslib/angular/pipes/search-ciphers.pipe';
FormsModule,
AppRoutingModule,
ServicesModule,
Angulartics2Module.forRoot([Angulartics2GoogleAnalytics], {
Angulartics2Module.forRoot({
pageTracking: {
clearQueryParams: true,
},
@@ -60,6 +62,7 @@ import { SearchCiphersPipe } from 'jslib/angular/pipes/search-ciphers.pipe';
AutofocusDirective,
BlurClickDirective,
BoxRowDirective,
CalloutComponent,
DashboardComponent,
EnvironmentComponent,
FallbackSrcDirective,
@@ -70,6 +73,7 @@ import { SearchCiphersPipe } from 'jslib/angular/pipes/search-ciphers.pipe';
MoreComponent,
SearchCiphersPipe,
SettingsComponent,
SsoComponent,
StopClickDirective,
StopPropDirective,
TabsComponent,

View File

@@ -33,6 +33,8 @@ import { ContainerService } from 'jslib/services/container.service';
import { CryptoService } from 'jslib/services/crypto.service';
import { EnvironmentService } from 'jslib/services/environment.service';
import { NodeCryptoFunctionService } from 'jslib/services/nodeCryptoFunction.service';
import { PasswordGenerationService } from 'jslib/services/passwordGeneration.service';
import { PolicyService } from 'jslib/services/policy.service';
import { StateService } from 'jslib/services/state.service';
import { TokenService } from 'jslib/services/token.service';
import { UserService } from 'jslib/services/user.service';
@@ -46,7 +48,11 @@ import { EnvironmentService as EnvironmentServiceAbstraction } from 'jslib/abstr
import { I18nService as I18nServiceAbstraction } from 'jslib/abstractions/i18n.service';
import { LogService as LogServiceAbstraction } from 'jslib/abstractions/log.service';
import { MessagingService as MessagingServiceAbstraction } from 'jslib/abstractions/messaging.service';
import {
PasswordGenerationService as PasswordGenerationServiceAbstraction,
} from 'jslib/abstractions/passwordGeneration.service';
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from 'jslib/abstractions/platformUtils.service';
import { PolicyService as PolicyServiceAbstraction } from 'jslib/abstractions/policy.service';
import { StateService as StateServiceAbstraction } from 'jslib/abstractions/state.service';
import { StorageService as StorageServiceAbstraction } from 'jslib/abstractions/storage.service';
import { TokenService as TokenServiceAbstraction } from 'jslib/abstractions/token.service';
@@ -57,11 +63,12 @@ const i18nService = new I18nService(window.navigator.language, './locales');
const stateService = new StateService();
const broadcasterService = new BroadcasterService();
const messagingService = new ElectronRendererMessagingService(broadcasterService);
const platformUtilsService = new ElectronPlatformUtilsService(i18nService, messagingService, true);
const storageService: StorageServiceAbstraction = new ElectronStorageService(remote.app.getPath('userData'));
const platformUtilsService = new ElectronPlatformUtilsService(i18nService, messagingService, false, storageService);
const secureStorageService: StorageServiceAbstraction = new ElectronRendererSecureStorageService();
const cryptoFunctionService: CryptoFunctionServiceAbstraction = new NodeCryptoFunctionService();
const cryptoService = new CryptoService(storageService, secureStorageService, cryptoFunctionService);
const cryptoService = new CryptoService(storageService, secureStorageService, cryptoFunctionService,
platformUtilsService);
const appIdService = new AppIdService(storageService);
const tokenService = new TokenService(storageService);
const apiService = new ApiService(tokenService, platformUtilsService,
@@ -70,10 +77,12 @@ const environmentService = new EnvironmentService(apiService, storageService, nu
const userService = new UserService(tokenService, storageService);
const containerService = new ContainerService(cryptoService);
const authService = new AuthService(cryptoService, apiService, userService, tokenService, appIdService,
i18nService, platformUtilsService, messagingService, false);
i18nService, platformUtilsService, messagingService, null, false);
const configurationService = new ConfigurationService(storageService, secureStorageService);
const syncService = new SyncService(configurationService, logService, cryptoFunctionService, apiService,
messagingService, i18nService);
const passwordGenerationService = new PasswordGenerationService(cryptoService, storageService, null);
const policyService = new PolicyService(userService, storageService);
const analytics = new Analytics(window, () => true, platformUtilsService, storageService, appIdService);
containerService.attachToWindow(window);
@@ -134,6 +143,9 @@ export function initFactory(): Function {
{ provide: LogServiceAbstraction, useValue: logService },
{ provide: ConfigurationService, useValue: configurationService },
{ provide: SyncService, useValue: syncService },
{ provide: PasswordGenerationServiceAbstraction, useValue: passwordGenerationService },
{ provide: CryptoFunctionServiceAbstraction, useValue: cryptoFunctionService },
{ provide: PolicyServiceAbstraction, useValue: policyService },
{
provide: APP_INITIALIZER,
useFactory: initFactory,

View File

@@ -6,7 +6,7 @@
<p>
{{'bitwardenDirectoryConnector' | i18n}}
<br /> {{'version' | i18n : version}}
<br /> &copy; 8bit Solutions LLC 2015-{{year}}
<br /> &copy; Bitwarden Inc. LLC 2015-{{year}}
</p>
<button class="btn btn-primary" type="button" (click)="update()" [disabled]="checkingForUpdate">
<i class="fa fa-download fa-fw" [hidden]="checkingForUpdate"></i>

View File

@@ -33,13 +33,45 @@
<label class="form-check-label" for="ad">{{'ldapAd' | i18n}}</label>
</div>
</div>
<div class="form-group" *ngIf="!ldap.ad">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="pagedSearch"
[(ngModel)]="ldap.pagedSearch" name="PagedSearch">
<label class="form-check-label"
for="pagedSearch">{{'ldapPagedResults' | i18n}}</label>
</div>
</div>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="ssl" [(ngModel)]="ldap.ssl" name="SSL">
<label class="form-check-label" for="ssl">{{'ldapSsl' | i18n}}</label>
<input class="form-check-input" type="checkbox" id="ldapEncrypted" [(ngModel)]="ldap.ssl"
name="Encrypted">
<label class="form-check-label" for="ldapEncrypted">{{'ldapEncrypted' | i18n}}</label>
</div>
</div>
<div class="ml-4" *ngIf="ldap.ssl">
<div class="form-group">
<div class="form-radio">
<input class="form-radio-input" type="radio" [value]="false" id="ssl"
[(ngModel)]="ldap.startTls" name="SSL">
<label class="form-radio-label" for="ssl">{{'ldapSsl' | i18n}}</label>
</div>
<div class="form-radio">
<input class="form-radio-input" type="radio" [value]="true" id="startTls"
[(ngModel)]="ldap.startTls" name="StartTLS">
<label class="form-radio-label" for="startTls">{{'ldapTls' | i18n}}</label>
</div>
</div>
<div class="ml-4" *ngIf="ldap.startTls">
<p>{{'ldapTlsUntrustedDesc' | i18n}}</p>
<div class="form-group">
<label for="tlsCaPath">{{'ldapTlsCa' | i18n}}</label>
<input type="file" class="form-control-file mb-2" id="tlsCaPath_file"
(change)="setSslPath('tlsCaPath')">
<input type="text" class="form-control" id="tlsCaPath" name="TLSCaPath"
[(ngModel)]="ldap.tlsCaPath">
</div>
</div>
<div class="ml-4" *ngIf="!ldap.startTls">
<p>{{'ldapSslUntrustedDesc' | i18n}}</p>
<div class="form-group">
<label for="sslCertPath">{{'ldapSslCert' | i18n}}</label>
@@ -62,12 +94,13 @@
<input type="text" class="form-control" id="sslCaPath" name="SSLCaPath"
[(ngModel)]="ldap.sslCaPath">
</div>
</div>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="sslAllowUnauthorized"
[(ngModel)]="ldap.sslAllowUnauthorized" name="SSLAllowUnauthorized">
<input class="form-check-input" type="checkbox" id="certDoNotVerify"
[(ngModel)]="ldap.sslAllowUnauthorized" name="CertDoNoVerify">
<label class="form-check-label"
for="sslAllowUnauthorized">{{'ldapSslAllowUnauthorized' | i18n}}</label>
for="certDoNotVerify">{{'ldapCertDoNotVerify' | i18n}}</label>
</div>
</div>
</div>
@@ -122,6 +155,26 @@
[(ngModel)]="okta.token">
</div>
</div>
<div [hidden]="directory != directoryType.OneLogin">
<div class="form-group">
<label for="oneLoginClientId">{{'clientId' | i18n}}</label>
<input type="text" class="form-control" id="oneLoginClientId" name="OneLoginClientId"
[(ngModel)]="oneLogin.clientId">
</div>
<div class="form-group">
<label for="oneLoginClientSecret">{{'clientSecret' | i18n}}</label>
<input type="text" class="form-control" id="oneLoginClientSecret" name="OneLoginClientSecret"
[(ngModel)]="oneLogin.clientSecret">
</div>
<div class="form-group">
<label for="oneLoginRegion">{{'region' | i18n}}</label>
<select class="form-control" id="oneLoginRegion" name="OneLoginRegion"
[(ngModel)]="oneLogin.region">
<option value="us">US</option>
<option value="eu">EU</option>
</select>
</div>
</div>
<div [hidden]="directory != directoryType.GSuite">
<div class="form-group">
<label for="domain">{{'domain' | i18n}}</label>
@@ -218,7 +271,8 @@
<small class="text-muted form-text">{{'ex' | i18n}} whenChanged</small>
</div>
</div>
</div>
<div [hidden]="directory != directoryType.Ldap && directory != directoryType.OneLogin">
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="useEmailPrefixSuffix"
@@ -228,7 +282,7 @@
</div>
</div>
<div [hidden]="!sync.useEmailPrefixSuffix">
<div class="form-group" [hidden]="ldap.ad">
<div class="form-group" [hidden]="ldap.ad || directory != directoryType.Ldap">
<label for="emailPrefixAttribute">{{'emailPrefixAttribute' | i18n}}</label>
<input type="text" class="form-control" id="emailPrefixAttribute"
name="EmailPrefixAttribute" [(ngModel)]="sync.emailPrefixAttribute">

View File

@@ -19,6 +19,7 @@ import { AzureConfiguration } from '../../models/azureConfiguration';
import { GSuiteConfiguration } from '../../models/gsuiteConfiguration';
import { LdapConfiguration } from '../../models/ldapConfiguration';
import { OktaConfiguration } from '../../models/oktaConfiguration';
import { OneLoginConfiguration } from '../../models/oneLoginConfiguration';
import { SyncConfiguration } from '../../models/syncConfiguration';
import { ConnectorUtils } from '../../utils';
@@ -34,6 +35,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
gsuite = new GSuiteConfiguration();
azure = new AzureConfiguration();
okta = new OktaConfiguration();
oneLogin = new OneLoginConfiguration();
sync = new SyncConfiguration();
organizationId: string;
directoryOptions: any[];
@@ -48,6 +50,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
{ name: 'Azure Active Directory', value: DirectoryType.AzureActiveDirectory },
{ name: 'G Suite (Google)', value: DirectoryType.GSuite },
{ name: 'Okta', value: DirectoryType.Okta },
{ name: 'OneLogin', value: DirectoryType.OneLogin },
];
}
@@ -70,6 +73,8 @@ export class SettingsComponent implements OnInit, OnDestroy {
DirectoryType.AzureActiveDirectory)) || this.azure;
this.okta = (await this.configurationService.getDirectory<OktaConfiguration>(
DirectoryType.Okta)) || this.okta;
this.oneLogin = (await this.configurationService.getDirectory<OneLoginConfiguration>(
DirectoryType.OneLogin)) || this.oneLogin;
this.sync = (await this.configurationService.getSync()) || this.sync;
}
@@ -79,12 +84,16 @@ export class SettingsComponent implements OnInit, OnDestroy {
async submit() {
ConnectorUtils.adjustConfigForSave(this.ldap, this.sync);
if (this.ldap != null && this.ldap.ad) {
this.ldap.pagedSearch = true;
}
await this.configurationService.saveOrganizationId(this.organizationId);
await this.configurationService.saveDirectoryType(this.directory);
await this.configurationService.saveDirectory(DirectoryType.Ldap, this.ldap);
await this.configurationService.saveDirectory(DirectoryType.GSuite, this.gsuite);
await this.configurationService.saveDirectory(DirectoryType.AzureActiveDirectory, this.azure);
await this.configurationService.saveDirectory(DirectoryType.Okta, this.okta);
await this.configurationService.saveDirectory(DirectoryType.OneLogin, this.oneLogin);
await this.configurationService.saveSync(this.sync);
}

View File

@@ -22,9 +22,12 @@ 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 { PasswordGenerationService } from 'jslib/services/passwordGeneration.service';
import { TokenService } from 'jslib/services/token.service';
import { UserService } from 'jslib/services/user.service';
import { StorageService as StorageServiceAbstraction } from 'jslib/abstractions/storage.service';
import { Program } from './program';
// tslint:disable-next-line
@@ -35,7 +38,7 @@ export class Main {
logService: ConsoleLogService;
messagingService: NoopMessagingService;
storageService: LowdbStorageService;
secureStorageService: KeytarSecureStorageService;
secureStorageService: StorageServiceAbstraction;
i18nService: I18nService;
platformUtilsService: CliPlatformUtilsService;
constantsService: ConstantsService;
@@ -50,6 +53,7 @@ export class Main {
authService: AuthService;
configurationService: ConfigurationService;
syncService: SyncService;
passwordGenerationService: PasswordGenerationService;
program: Program;
constructor() {
@@ -70,15 +74,17 @@ export class Main {
this.dataFilePath = path.join(process.env.HOME, '.config/' + applicationName);
}
const plaintextSecrets = process.env.BITWARDENCLI_CONNECTOR_PLAINTEXT_SECRETS === 'true';
this.i18nService = new I18nService('en', './locales');
this.platformUtilsService = new CliPlatformUtilsService('connector', packageJson);
this.logService = new ConsoleLogService(this.platformUtilsService.isDev(),
(level) => process.env.BWCLI_DEBUG !== 'true' && level <= LogLevelType.Info);
(level) => process.env.BITWARDENCLI_CONNECTOR_DEBUG !== 'true' && level <= LogLevelType.Info);
this.cryptoFunctionService = new NodeCryptoFunctionService();
this.storageService = new LowdbStorageService(null, this.dataFilePath, true);
this.secureStorageService = new KeytarSecureStorageService(applicationName);
this.storageService = new LowdbStorageService(this.logService, null, this.dataFilePath, true);
this.secureStorageService = plaintextSecrets ?
this.storageService : new KeytarSecureStorageService(applicationName);
this.cryptoService = new CryptoService(this.storageService, this.secureStorageService,
this.cryptoFunctionService);
this.cryptoFunctionService, this.platformUtilsService);
this.appIdService = new AppIdService(this.storageService);
this.tokenService = new TokenService(this.storageService);
this.messagingService = new NoopMessagingService();
@@ -88,10 +94,12 @@ export class Main {
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.appIdService, this.i18nService, this.platformUtilsService, this.messagingService, null, false);
this.configurationService = new ConfigurationService(this.storageService, this.secureStorageService,
process.env.BITWARDENCLI_CONNECTOR_PLAINTEXT_SECRETS !== 'true');
this.syncService = new SyncService(this.configurationService, this.logService, this.cryptoFunctionService,
this.apiService, this.messagingService, this.i18nService);
this.passwordGenerationService = new PasswordGenerationService(this.cryptoService, this.storageService, null);
this.program = new Program(this);
}

View File

@@ -14,6 +14,7 @@ import { AzureConfiguration } from '../models/azureConfiguration';
import { GSuiteConfiguration } from '../models/gsuiteConfiguration';
import { LdapConfiguration } from '../models/ldapConfiguration';
import { OktaConfiguration } from '../models/oktaConfiguration';
import { OneLoginConfiguration } from '../models/oneLoginConfiguration';
import { SyncConfiguration } from '../models/syncConfiguration';
import { ConnectorUtils } from '../utils';
@@ -24,6 +25,7 @@ export class ConfigCommand {
private gsuite = new GSuiteConfiguration();
private azure = new AzureConfiguration();
private okta = new OktaConfiguration();
private oneLogin = new OneLoginConfiguration();
private sync = new SyncConfiguration();
constructor(private environmentService: EnvironmentService, private i18nService: I18nService,
@@ -51,6 +53,9 @@ export class ConfigCommand {
case 'okta.token':
await this.setOktaToken(value);
break;
case 'onelogin.secret':
await this.setOneLoginSecret(value);
break;
default:
return Response.badRequest('Unknown setting.');
}
@@ -70,7 +75,7 @@ export class ConfigCommand {
private async setDirectory(type: string) {
const dir = parseInt(type, null);
if (dir < DirectoryType.Ldap || dir > DirectoryType.Okta) {
if (dir < DirectoryType.Ldap || dir > DirectoryType.OneLogin) {
throw new Error('Invalid directory type value.');
}
await this.loadConfig();
@@ -102,6 +107,12 @@ export class ConfigCommand {
await this.saveConfig();
}
private async setOneLoginSecret(secret: string) {
await this.loadConfig();
this.oneLogin.clientSecret = secret;
await this.saveConfig();
}
private async loadConfig() {
this.directory = await this.configurationService.getDirectoryType();
this.ldap = (await this.configurationService.getDirectory<LdapConfiguration>(DirectoryType.Ldap)) ||
@@ -112,6 +123,8 @@ export class ConfigCommand {
DirectoryType.AzureActiveDirectory)) || this.azure;
this.okta = (await this.configurationService.getDirectory<OktaConfiguration>(
DirectoryType.Okta)) || this.okta;
this.oneLogin = (await this.configurationService.getDirectory<OneLoginConfiguration>(
DirectoryType.OneLogin)) || this.oneLogin;
this.sync = (await this.configurationService.getSync()) || this.sync;
}
@@ -122,6 +135,7 @@ export class ConfigCommand {
await this.configurationService.saveDirectory(DirectoryType.GSuite, this.gsuite);
await this.configurationService.saveDirectory(DirectoryType.AzureActiveDirectory, this.azure);
await this.configurationService.saveDirectory(DirectoryType.Okta, this.okta);
await this.configurationService.saveDirectory(DirectoryType.OneLogin, this.oneLogin);
await this.configurationService.saveSync(this.sync);
}
}

View File

@@ -3,4 +3,5 @@ export enum DirectoryType {
AzureActiveDirectory = 1,
GSuite = 2,
Okta = 3,
OneLogin = 4,
}

View File

@@ -140,6 +140,9 @@
"baseUrl": {
"message": "Server URL"
},
"webVaultUrl": {
"message": "Web Vault Server URL"
},
"apiUrl": {
"message": "API Server URL"
},
@@ -414,8 +417,20 @@
"sync": {
"message": "Sync"
},
"ldapEncrypted": {
"message": "This server uses an encrypted connection"
},
"ldapTls": {
"message": "Use TLS (STARTTLS)"
},
"ldapTlsCa": {
"message": "Certificate CA Chain (PEM)"
},
"ldapSsl": {
"message": "This server uses SSL (LDAPS)"
"message": "Use SSL (LDAPS)"
},
"ldapTlsUntrustedDesc": {
"message": "If your LDAP server uses a self-signed certificate for STARTTLS, you can configure certificate options below."
},
"ldapSslUntrustedDesc": {
"message": "If your LDAPS server uses an untrusted certificate you can configure certificate options below."
@@ -429,12 +444,15 @@
"ldapSslKey": {
"message": "Certificate Private Key (PEM)"
},
"ldapSslAllowUnauthorized": {
"message": "Allow untrusted SSL connections (not recommended)."
"ldapCertDoNotVerify": {
"message": "Do not verify server certificates (not recommended)."
},
"ldapAd": {
"message": "This server uses Active Directory"
},
"ldapPagedResults": {
"message": "This server pages search results"
},
"select": {
"message": "Select"
},
@@ -587,5 +605,122 @@
},
"overwriteExisting": {
"message": "Overwrite existing organization users based on current sync settings."
},
"clientId": {
"message": "Client ID"
},
"clientSecret": {
"message": "Client Secret"
},
"region": {
"message": "Region"
},
"enterpriseSingleSignOn": {
"message": "Enterprise Single Sign-On"
},
"setMasterPassword": {
"message": "Set Master Password"
},
"ssoCompleteRegistration": {
"message": "In order to complete logging in with SSO, please set a master password to access and protect your vault."
},
"newMasterPass": {
"message": "New Master Password"
},
"confirmNewMasterPass": {
"message": "Confirm New Master Password"
},
"masterPasswordPolicyInEffect": {
"message": "One or more organization policies require your master password to meet the following requirements:"
},
"policyInEffectMinComplexity": {
"message": "Minimum complexity score of $SCORE$",
"placeholders": {
"score": {
"content": "$1",
"example": "4"
}
}
},
"policyInEffectMinLength": {
"message": "Minimum length of $LENGTH$",
"placeholders": {
"length": {
"content": "$1",
"example": "14"
}
}
},
"policyInEffectUppercase": {
"message": "Contain one or more uppercase characters"
},
"policyInEffectLowercase": {
"message": "Contain one or more lowercase characters"
},
"policyInEffectNumbers": {
"message": "Contain one or more numbers"
},
"policyInEffectSpecial": {
"message": "Contain one or more of the following special characters $CHARS$",
"placeholders": {
"chars": {
"content": "$1",
"example": "!@#$%^&*"
}
}
},
"masterPassDesc": {
"message": "The master password is the password you use to access your vault. It is very important that you do not forget your master password. There is no way to recover the password in the event that you forget it."
},
"reTypeMasterPass": {
"message": "Re-type Master Password"
},
"masterPassHint": {
"message": "Master Password Hint (optional)"
},
"masterPassHintDesc": {
"message": "A master password hint can help you remember your password if you forget it."
},
"strong": {
"message": "Strong",
"description": "ex. A strong password. Scale: Weak -> Good -> Strong"
},
"good": {
"message": "Good",
"description": "ex. A good password. Scale: Weak -> Good -> Strong"
},
"weak": {
"message": "Weak",
"description": "ex. A weak password. Scale: Weak -> Good -> Strong"
},
"weakMasterPassword": {
"message": "Weak Master Password"
},
"weakMasterPasswordDesc": {
"message": "The master password you have chosen is weak. You should use a strong master password (or a passphrase) to properly protect your Bitwarden account. Are you sure you want to use this master password?"
},
"errorOccurred": {
"message": "An error has occurred."
},
"error": {
"message": "Error"
},
"masterPassLength": {
"message": "Master password must be at least 8 characters long."
},
"masterPassDoesntMatch": {
"message": "Master password confirmation does not match."
},
"masterPasswordPolicyRequirementsNotMet": {
"message": "Your new master password does not meet the policy requirements."
},
"loading": {
"message": "Loading"
},
"setMasterPasswordRedirect": {
"message": "In order to log in with SSO from the Directory Connector, you must first log in through the web vault to set your master password."
},
"launchWebVault": {
"message": "Launch Web Vault"
}
}

View File

@@ -52,7 +52,8 @@ export class Main {
this.i18nService = new I18nService('en', './locales/');
this.storageService = new ElectronStorageService(app.getPath('userData'));
this.windowMain = new WindowMain(this.storageService, false, 800, 600);
this.windowMain = new WindowMain(this.storageService, false, 800, 600,
(arg) => this.processDeepLink(arg));
this.menuMain = new MenuMain(this);
this.updaterMain = new UpdaterMain(this.i18nService, this.windowMain, 'directory-connector', () => {
this.messagingService.send('checkingForUpdate');
@@ -78,11 +79,32 @@ export class Main {
this.messagingMain.init();
await this.updaterMain.init();
await this.trayMain.init(this.i18nService.t('bitwardenDirectoryConnector'));
if (!app.isDefaultProtocolClient('bwdc')) {
app.setAsDefaultProtocolClient('bwdc');
}
// Process protocol for macOS
app.on('open-url', (event, url) => {
event.preventDefault();
this.processDeepLink([url]);
});
}, (e: any) => {
// tslint:disable-next-line
console.error(e);
});
}
private processDeepLink(argv: string[]): void {
argv.filter((s) => s.indexOf('bwdc://') === 0).forEach((s) => {
const url = new URL(s);
const code = url.searchParams.get('code');
const receivedState = url.searchParams.get('state');
if (code != null && receivedState != null) {
this.messagingService.send('ssoCallback', { code: code, state: receivedState });
}
});
}
}
const main = new Main();

View File

@@ -1,5 +1,7 @@
export class LdapConfiguration {
ssl = false;
startTls = false;
tlsCaPath: string;
sslAllowUnauthorized = false;
sslCertPath: string;
sslKeyPath: string;
@@ -12,4 +14,5 @@ export class LdapConfiguration {
username: string;
password: string;
ad = true;
pagedSearch = true;
}

View File

@@ -0,0 +1,5 @@
export class OneLoginConfiguration {
clientId: string;
clientSecret: string;
region = 'us';
}

View File

@@ -2,8 +2,8 @@
"name": "bitwarden-directory-connector",
"productName": "Bitwarden Directory Connector",
"description": "Sync your user directory to your Bitwarden organization.",
"version": "2.6.1",
"author": "8bit Solutions LLC <hello@bitwarden.com> (https://bitwarden.com)",
"version": "2.8.2",
"author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)",
"homepage": "https://bitwarden.com",
"license": "GPL-3.0",
"main": "main.js",
@@ -14,7 +14,7 @@
"dependencies": {
"electron-log": "2.2.17",
"electron-store": "1.3.0",
"electron-updater": "4.0.6",
"keytar": "4.4.1"
"electron-updater": "4.2.0",
"keytar": "4.13.0"
}
}

View File

@@ -40,6 +40,7 @@ export class Program extends BaseProgram {
.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.')
.option('--nointeraction', 'Do not prompt for interactive user input.')
.version(this.main.platformUtilsService.getApplicationVersion(), '-v, --version');
program.on('option:pretty', () => {
@@ -58,6 +59,10 @@ export class Program extends BaseProgram {
process.env.BW_RESPONSE = 'true';
});
program.on('option:nointeraction', () => {
process.env.BW_NOINTERACTION = 'true';
});
program.on('command:*', () => {
writeLn(chalk.redBright('Invalid command: ' + program.args.join(' ')), false, true);
writeLn('See --help for a list of available commands.', true, true);
@@ -81,6 +86,7 @@ export class Program extends BaseProgram {
.description('Log into a user account.')
.option('--method <method>', 'Two-step login method.')
.option('--code <code>', 'Two-step login code.')
.option('--sso', 'Log in with Single-Sign On.')
.on('--help', () => {
writeLn('\n Notes:');
writeLn('');
@@ -91,11 +97,14 @@ export class Program extends BaseProgram {
writeLn(' bw login');
writeLn(' bw login john@example.com myPassword321');
writeLn(' bw login john@example.com myPassword321 --method 1 --code 249213');
writeLn(' bw login --sso');
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 command = new LoginCommand(this.main.authService, this.main.apiService, this.main.i18nService,
this.main.environmentService, this.main.passwordGenerationService, this.main.cryptoFunctionService,
this.main.platformUtilsService, 'connector');
const response = await command.run(email, password, cmd);
this.processResponse(response);
});
@@ -184,6 +193,7 @@ export class Program extends BaseProgram {
writeLn(' azure.key - The Azure AD secret key.');
writeLn(' gsuite.key - The G Suite private key.');
writeLn(' okta.token - The Okta token.');
writeLn(' onelogin.secret - The OneLogin client secret.');
writeLn('');
writeLn(' Examples:');
writeLn('');
@@ -194,6 +204,7 @@ export class Program extends BaseProgram {
writeLn(' bwdc config azure.key <key>');
writeLn(' bwdc config gsuite.key <key>');
writeLn(' bwdc config okta.token <token>');
writeLn(' bwdc config onelogin.secret <secret>');
writeLn('', true);
})
.action(async (setting, value, cmd) => {

View File

@@ -1,4 +1,4 @@
$theme-colors: ( "primary": #3c8dbc, "primary-accent": #286090, "danger": #dd4b39, "success": #00a65a, "info": #555555, "warning": #bf7e16);
$theme-colors: ( "primary": #175DDC, "primary-accent": #1252A3, "danger": #dd4b39, "success": #00a65a, "info": #555555, "warning": #bf7e16, "secondary": #ced4da, "secondary-alt": #1A3B66);
$font-family-sans-serif: 'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
$h1-font-size: 2rem;
@@ -8,4 +8,13 @@ $h4-font-size: 1rem;
$h5-font-size: 1rem;
$h6-font-size: 1rem;
$primary: map_get($theme-colors, 'primary');
$primary-accent: map_get($theme-colors, 'primary-accent');
$success: map_get($theme-colors, 'success');
$info: map_get($theme-colors, 'info');
$warning: map_get($theme-colors, 'warning');
$danger: map_get($theme-colors, 'danger');
$secondary: map_get($theme-colors, 'secondary');
$secondary-alt: map_get($theme-colors, 'secondary-alt');
@import "~bootstrap/scss/bootstrap.scss";

View File

@@ -53,3 +53,68 @@ ul.testing-list {
text-decoration: line-through;
}
}
.callout {
padding: 10px;
margin-bottom: 10px;
border: 1px solid #000000;
border-left-width: 5px;
border-radius: 3px;
border-color: #ddd;
background-color: white;
.callout-heading {
margin-top: 0;
}
h3.callout-heading {
font-weight: bold;
text-transform: uppercase;
}
&.callout-primary {
border-left-color: $primary;
.callout-heading {
color: $primary;
}
}
&.callout-info {
border-left-color: $info;
.callout-heading {
color: $info;
}
}
&.callout-danger {
border-left-color: $danger;
.callout-heading {
color: $danger;
}
}
&.callout-success {
border-left-color: $success;
.callout-heading {
color: $success;
}
}
&.callout-warning {
border-left-color: $warning;
.callout-heading {
color: $warning;
}
}
ul {
padding-left: 40px;
margin: 0;
}
}

View File

@@ -20,6 +20,7 @@ import { LogService } from 'jslib/abstractions/log.service';
const NextLink = '@odata.nextLink';
const DeltaLink = '@odata.deltaLink';
const ObjectType = '@odata.type';
const UserSelectParams = '?$select=id,mail,userPrincipalName,displayName,accountEnabled';
enum UserSetType {
IncludeUser,
@@ -69,7 +70,7 @@ export class AzureDirectoryService extends BaseDirectoryService implements Direc
if (this.syncConfig.groups) {
const setFilter = this.createCustomSet(this.syncConfig.groupFilter);
groups = await this.getGroups(setFilter);
users = this.filterUsersFromGroupsSet(users, groups, setFilter);
users = this.filterUsersFromGroupsSet(users, groups, setFilter, this.syncConfig);
}
return [groups, users];
@@ -78,7 +79,7 @@ export class AzureDirectoryService extends BaseDirectoryService implements Direc
private async getCurrentUsers(): Promise<UserEntry[]> {
const entryIds = new Set<string>();
const entries: UserEntry[] = [];
const userReq = this.client.api('/users');
const userReq = this.client.api('/users' + UserSelectParams);
let res = await userReq.get();
const setFilter = this.createCustomUserSet(this.syncConfig.userFilter);
while (true) {
@@ -130,7 +131,7 @@ export class AzureDirectoryService extends BaseDirectoryService implements Direc
}
if (res == null) {
const userReq = this.client.api('/users/delta');
const userReq = this.client.api('/users/delta' + UserSelectParams);
res = await userReq.get();
}
@@ -305,7 +306,8 @@ export class AzureDirectoryService extends BaseDirectoryService implements Direc
entry.name = group.displayName;
const memReq = this.client.api('/groups/' + group.id + '/members');
const memRes = await memReq.get();
let memRes = await memReq.get();
while (true) {
const members: any = memRes.value;
if (members != null) {
for (const member of members) {
@@ -316,6 +318,13 @@ export class AzureDirectoryService extends BaseDirectoryService implements Direc
}
}
}
if (memRes[NextLink] == null) {
break;
} else {
const nextMemReq = this.client.api(memRes[NextLink]);
memRes = await nextMemReq.get();
}
}
return entry;
}

View File

@@ -1,3 +1,5 @@
import { SyncConfiguration } from '../models/syncConfiguration';
import { GroupEntry } from '../models/groupEntry';
import { UserEntry } from '../models/userEntry';
@@ -66,13 +68,16 @@ export abstract class BaseDirectoryService {
}
protected filterUsersFromGroupsSet(users: UserEntry[], groups: GroupEntry[],
setFilter: [boolean, Set<string>]): UserEntry[] {
setFilter: [boolean, Set<string>], syncConfig: SyncConfiguration): UserEntry[] {
if (setFilter == null || users == null) {
return users;
}
return users.filter((u) => {
if (u.disabled || u.deleted) {
if (u.deleted) {
return true;
}
if (u.disabled && syncConfig.removeDisabled) {
return true;
}

View File

@@ -5,6 +5,7 @@ import { AzureConfiguration } from '../models/azureConfiguration';
import { GSuiteConfiguration } from '../models/gsuiteConfiguration';
import { LdapConfiguration } from '../models/ldapConfiguration';
import { OktaConfiguration } from '../models/oktaConfiguration';
import { OneLoginConfiguration } from '../models/oneLoginConfiguration';
import { SyncConfiguration } from '../models/syncConfiguration';
const StoredSecurely = '[STORED SECURELY]';
@@ -13,6 +14,7 @@ const Keys = {
gsuite: 'gsuitePrivateKey',
azure: 'azureKey',
okta: 'oktaToken',
oneLogin: 'oneLoginClientSecret',
directoryConfigPrefix: 'directoryConfig_',
sync: 'syncConfig',
directoryType: 'directoryType',
@@ -25,7 +27,8 @@ const Keys = {
};
export class ConfigurationService {
constructor(private storageService: StorageService, private secureStorageService: StorageService) { }
constructor(private storageService: StorageService, private secureStorageService: StorageService,
private useSecureStorageForSecrets = true) { }
async getDirectory<T>(type: DirectoryType): Promise<T> {
const config = await this.storageService.get<T>(Keys.directoryConfigPrefix + type);
@@ -33,6 +36,7 @@ export class ConfigurationService {
return config;
}
if (this.useSecureStorageForSecrets) {
switch (type) {
case DirectoryType.Ldap:
(config as any).password = await this.secureStorageService.get<string>(Keys.ldap);
@@ -46,13 +50,19 @@ export class ConfigurationService {
case DirectoryType.GSuite:
(config as any).privateKey = await this.secureStorageService.get<string>(Keys.gsuite);
break;
case DirectoryType.OneLogin:
(config as any).clientSecret = await this.secureStorageService.get<string>(Keys.oneLogin);
break;
}
}
return config;
}
async saveDirectory(type: DirectoryType,
config: LdapConfiguration | GSuiteConfiguration | AzureConfiguration | OktaConfiguration): Promise<any> {
config: LdapConfiguration | GSuiteConfiguration | AzureConfiguration | OktaConfiguration |
OneLoginConfiguration): Promise<any> {
const savedConfig: any = Object.assign({}, config);
if (this.useSecureStorageForSecrets) {
switch (type) {
case DirectoryType.Ldap:
if (savedConfig.password == null) {
@@ -88,6 +98,15 @@ export class ConfigurationService {
savedConfig.privateKey = StoredSecurely;
}
break;
case DirectoryType.OneLogin:
if (savedConfig.clientSecret == null) {
await this.secureStorageService.remove(Keys.oneLogin);
} else {
await this.secureStorageService.save(Keys.oneLogin, savedConfig.clientSecret);
savedConfig.clientSecret = StoredSecurely;
}
break;
}
}
await this.storageService.save(Keys.directoryConfigPrefix + type, savedConfig);
}

View File

@@ -49,7 +49,7 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements Dire
await this.auth();
let users: UserEntry[];
let users: UserEntry[] = [];
if (this.syncConfig.users) {
users = await this.getUsers();
}
@@ -57,8 +57,8 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements Dire
let groups: GroupEntry[];
if (this.syncConfig.groups) {
const setFilter = this.createCustomSet(this.syncConfig.groupFilter);
groups = await this.getGroups(setFilter);
users = this.filterUsersFromGroupsSet(users, groups, setFilter);
groups = await this.getGroups(setFilter, users);
users = this.filterUsersFromGroupsSet(users, groups, setFilter, this.syncConfig);
}
return [groups, users];
@@ -140,7 +140,7 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements Dire
return entry;
}
private async getGroups(setFilter: [boolean, Set<string>]): Promise<GroupEntry[]> {
private async getGroups(setFilter: [boolean, Set<string>], users: UserEntry[]): Promise<GroupEntry[]> {
const entries: GroupEntry[] = [];
let nextPageToken: string = null;
@@ -156,7 +156,7 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements Dire
if (res.data.groups != null) {
for (const group of res.data.groups) {
if (!this.filterOutResult(setFilter, group.name)) {
const entry = await this.buildGroup(group);
const entry = await this.buildGroup(group, users);
entries.push(entry);
}
}
@@ -170,7 +170,7 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements Dire
return entries;
}
private async buildGroup(group: admin_directory_v1.Schema$Group) {
private async buildGroup(group: admin_directory_v1.Schema$Group, users: UserEntry[]) {
let nextPageToken: string = null;
const entry = new GroupEntry();
@@ -192,18 +192,18 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements Dire
if (member.type == null) {
continue;
}
if (member.role == null || member.role.toLowerCase() !== 'member') {
continue;
}
const type = member.type.toLowerCase();
if (type === 'user') {
if (member.status == null || member.status.toLowerCase() !== 'active') {
continue;
}
const type = member.type.toLowerCase();
if (type === 'user') {
entry.userMemberExternalIds.add(member.id);
} else if (type === 'group') {
entry.groupMemberReferenceIds.add(member.id);
} else if (type === 'customer') {
for (const user of users) {
entry.userMemberExternalIds.add(user.externalId);
}
}
}
}

View File

@@ -231,6 +231,9 @@ export class LdapDirectoryService implements DirectoryService {
}
private makeSearchPath(pathPrefix: string) {
if (this.dirConfig.rootPath.toLowerCase().indexOf('dc=') === -1) {
return pathPrefix;
}
if (this.dirConfig.rootPath != null && this.dirConfig.rootPath.trim() !== '') {
const trimmedRootPath = this.dirConfig.rootPath.trim().toLowerCase();
let path = trimmedRootPath.substr(trimmedRootPath.indexOf('dc='));
@@ -290,7 +293,7 @@ export class LdapDirectoryService implements DirectoryService {
const options: ldap.SearchOptions = {
filter: filter,
scope: 'sub',
paged: true,
paged: this.dirConfig.pagedSearch,
};
const entries: T[] = [];
return new Promise<T[]>((resolve, reject) => {
@@ -324,17 +327,19 @@ export class LdapDirectoryService implements DirectoryService {
reject(this.i18nService.t('dirConfigIncomplete'));
return;
}
const url = 'ldap' + (this.dirConfig.ssl ? 's' : '') + '://' + this.dirConfig.hostname +
const protocol = 'ldap' + (this.dirConfig.ssl && !this.dirConfig.startTls ? 's' : '');
const url = protocol + '://' + this.dirConfig.hostname +
':' + this.dirConfig.port;
const options: ldap.ClientOptions = {
url: url.trim().toLowerCase(),
};
if (this.dirConfig.ssl) {
const tlsOptions: any = {};
if (this.dirConfig.sslAllowUnauthorized != null) {
if (this.dirConfig.ssl) {
if (this.dirConfig.sslAllowUnauthorized) {
tlsOptions.rejectUnauthorized = !this.dirConfig.sslAllowUnauthorized;
}
if (!this.dirConfig.startTls) {
if (this.dirConfig.sslCaPath != null && this.dirConfig.sslCaPath !== '' &&
fs.existsSync(this.dirConfig.sslCaPath)) {
tlsOptions.ca = [fs.readFileSync(this.dirConfig.sslCaPath)];
@@ -347,10 +352,17 @@ export class LdapDirectoryService implements DirectoryService {
fs.existsSync(this.dirConfig.sslKeyPath)) {
tlsOptions.key = fs.readFileSync(this.dirConfig.sslKeyPath);
}
} else {
if (this.dirConfig.tlsCaPath != null && this.dirConfig.tlsCaPath !== '' &&
fs.existsSync(this.dirConfig.tlsCaPath)) {
tlsOptions.ca = [fs.readFileSync(this.dirConfig.tlsCaPath)];
}
}
}
if (Object.keys(tlsOptions).length > 0) {
options.tlsOptions = tlsOptions;
}
}
this.client = ldap.createClient(options);
@@ -364,6 +376,21 @@ export class LdapDirectoryService implements DirectoryService {
return;
}
if (this.dirConfig.startTls && this.dirConfig.ssl) {
this.client.starttls(options.tlsOptions, undefined, (err, res) => {
if (err != null) {
reject(err.message);
} else {
this.client.bind(user, pass, (err2) => {
if (err2 != null) {
reject(err2.message);
} else {
resolve();
}
});
}
});
} else {
this.client.bind(user, pass, (err) => {
if (err != null) {
reject(err.message);
@@ -371,6 +398,7 @@ export class LdapDirectoryService implements DirectoryService {
resolve();
}
});
}
});
}

View File

@@ -12,13 +12,11 @@ import { DirectoryService } from './directory.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { LogService } from 'jslib/abstractions/log.service';
// tslint:disable-next-line
const okta = require('@okta/okta-sdk-nodejs');
import * as https from 'https';
export class OktaDirectoryService extends BaseDirectoryService implements DirectoryService {
private dirConfig: OktaConfiguration;
private syncConfig: SyncConfiguration;
private client: any;
constructor(private configurationService: ConfigurationService, private logService: LogService,
private i18nService: I18nService) {
@@ -45,11 +43,6 @@ export class OktaDirectoryService extends BaseDirectoryService implements Direct
throw new Error(this.i18nService.t('dirConfigIncomplete'));
}
this.client = new okta.Client({
orgUrl: this.dirConfig.orgUrl,
token: this.dirConfig.token,
});
let users: UserEntry[];
if (this.syncConfig.users) {
users = await this.getUsers(force);
@@ -59,7 +52,7 @@ export class OktaDirectoryService extends BaseDirectoryService implements Direct
if (this.syncConfig.groups) {
const setFilter = this.createCustomSet(this.syncConfig.groupFilter);
groups = await this.getGroups(this.forceGroup(force, users), setFilter);
users = this.filterUsersFromGroupsSet(users, groups, setFilter);
users = this.filterUsersFromGroupsSet(users, groups, setFilter, this.syncConfig);
}
return [groups, users];
@@ -72,11 +65,14 @@ export class OktaDirectoryService extends BaseDirectoryService implements Direct
const setFilter = this.createCustomSet(this.syncConfig.userFilter);
this.logService.info('Querying users.');
const usersPromise = this.client.listUsers({ filter: oktaFilter }).each((user: any) => {
const usersPromise = this.apiGetMany('users?filter=' + this.encodeUrlParameter(oktaFilter))
.then((users: any[]) => {
for (const user of users) {
const entry = this.buildUser(user);
if (entry != null && !this.filterOutResult(setFilter, entry.email)) {
entries.push(entry);
}
}
});
// Deactivated users have to be queried for separately, only when no filter is provided in the first query
@@ -86,11 +82,14 @@ export class OktaDirectoryService extends BaseDirectoryService implements Direct
if (oktaFilter != null) {
deactOktaFilter = '(' + oktaFilter + ') and ' + deactOktaFilter;
}
deactUsersPromise = this.client.listUsers({ filter: deactOktaFilter }).each((user: any) => {
deactUsersPromise = this.apiGetMany('users?filter=' + this.encodeUrlParameter(deactOktaFilter))
.then((users: any[]) => {
for (const user of users) {
const entry = this.buildUser(user);
if (entry != null && !this.filterOutResult(setFilter, entry.email)) {
entries.push(entry);
}
}
});
} else {
deactUsersPromise = Promise.resolve();
@@ -116,11 +115,15 @@ export class OktaDirectoryService extends BaseDirectoryService implements Direct
const oktaFilter = this.buildOktaFilter(this.syncConfig.groupFilter, force, lastSync);
this.logService.info('Querying groups.');
await this.client.listGroups({ filter: oktaFilter }).each(async (group: any) => {
await this.apiGetMany('groups?filter=' + this.encodeUrlParameter(oktaFilter)).then(async (groups: any[]) => {
for (const group of groups) {
const entry = await this.buildGroup(group);
if (entry != null && !this.filterOutResult(setFilter, entry.name)) {
entries.push(entry);
}
// throttle some to avoid rate limiting
await new Promise((resolve) => setTimeout(resolve, 500));
}
});
return entries;
}
@@ -131,8 +134,10 @@ export class OktaDirectoryService extends BaseDirectoryService implements Direct
entry.referenceId = group.id;
entry.name = group.profile.name;
await this.client.listGroupUsers(group.id).each((user: any) => {
await this.apiGetMany('groups/' + group.id + '/users').then((users: any[]) => {
for (const user of users) {
entry.userMemberExternalIds.add(user.id);
}
});
return entry;
@@ -152,4 +157,88 @@ export class OktaDirectoryService extends BaseDirectoryService implements Direct
return '(' + baseFilter + ') and ' + updatedFilter;
}
private encodeUrlParameter(filter: string): string {
return filter == null ? '' : encodeURIComponent(filter);
}
private async apiGetCall(url: string): Promise<[any, Map<string, string | string[]>]> {
const u = new URL(url);
return new Promise((resolve) => {
https.get({
hostname: u.hostname,
path: u.pathname + u.search,
port: 443,
headers: {
Authorization: 'SSWS ' + this.dirConfig.token,
Accept: 'application/json',
},
}, (res) => {
let body = '';
res.on('data', (chunk) => {
body += chunk;
});
res.on('end', () => {
if (res.statusCode !== 200) {
resolve(null);
return;
}
const responseJson = JSON.parse(body);
if (res.headers != null) {
const headersMap = new Map<string, string | string[]>();
for (const key in res.headers) {
if (res.headers.hasOwnProperty(key)) {
const val = res.headers[key];
headersMap.set(key.toLowerCase(), val);
}
}
resolve([responseJson, headersMap]);
return;
}
resolve([responseJson, null]);
});
res.on('error', () => {
resolve(null);
});
});
});
}
private async apiGetMany(endpoint: string, currentData: any[] = []): Promise<any[]> {
const url = endpoint.indexOf('https://') === 0 ? endpoint : `${this.dirConfig.orgUrl}/api/v1/${endpoint}`;
const response = await this.apiGetCall(url);
if (response == null || response[0] == null || !Array.isArray(response[0])) {
throw new Error('API call failed.');
}
if (response[0].length === 0) {
return currentData;
}
currentData = currentData.concat(response[0]);
if (response[1] == null) {
return currentData;
}
const linkHeader = response[1].get('link');
if (linkHeader == null || Array.isArray(linkHeader)) {
return currentData;
}
let nextLink: string = null;
const linkHeaderParts = linkHeader.split(',');
for (const part of linkHeaderParts) {
if (part.indexOf('; rel="next"') > -1) {
const subParts = part.split(';');
if (subParts.length > 0 && subParts[0].indexOf('https://') > -1) {
nextLink = subParts[0].replace('>', '').replace('<', '').trim();
break;
}
}
}
if (nextLink == null) {
return currentData;
}
return this.apiGetMany(nextLink, currentData);
}
}

View File

@@ -0,0 +1,195 @@
import { DirectoryType } from '../enums/directoryType';
import { GroupEntry } from '../models/groupEntry';
import { OneLoginConfiguration } from '../models/oneLoginConfiguration';
import { SyncConfiguration } from '../models/syncConfiguration';
import { UserEntry } from '../models/userEntry';
import { BaseDirectoryService } from './baseDirectory.service';
import { ConfigurationService } from './configuration.service';
import { DirectoryService } from './directory.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { LogService } from 'jslib/abstractions/log.service';
// Basic email validation: something@something.something
const ValidEmailRegex = /^\S+@\S+\.\S+$/;
export class OneLoginDirectoryService extends BaseDirectoryService implements DirectoryService {
private dirConfig: OneLoginConfiguration;
private syncConfig: SyncConfiguration;
private accessToken: string;
private allUsers: any[] = [];
constructor(private configurationService: ConfigurationService, private logService: LogService,
private i18nService: I18nService) {
super();
}
async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> {
const type = await this.configurationService.getDirectoryType();
if (type !== DirectoryType.OneLogin) {
return;
}
this.dirConfig = await this.configurationService.getDirectory<OneLoginConfiguration>(DirectoryType.OneLogin);
if (this.dirConfig == null) {
return;
}
this.syncConfig = await this.configurationService.getSync();
if (this.syncConfig == null) {
return;
}
if (this.dirConfig.clientId == null || this.dirConfig.clientSecret == null) {
throw new Error(this.i18nService.t('dirConfigIncomplete'));
}
this.accessToken = await this.getAccessToken();
if (this.accessToken == null) {
throw new Error('Could not get access token');
}
let users: UserEntry[];
if (this.syncConfig.users) {
users = await this.getUsers(force);
}
let groups: GroupEntry[];
if (this.syncConfig.groups) {
const setFilter = this.createCustomSet(this.syncConfig.groupFilter);
groups = await this.getGroups(this.forceGroup(force, users), setFilter);
users = this.filterUsersFromGroupsSet(users, groups, setFilter, this.syncConfig);
}
return [groups, users];
}
private async getUsers(force: boolean): Promise<UserEntry[]> {
const entries: UserEntry[] = [];
const query = this.createDirectoryQuery(this.syncConfig.userFilter);
const setFilter = this.createCustomSet(this.syncConfig.userFilter);
this.logService.info('Querying users.');
this.allUsers = await this.apiGetMany('users' + (query != null ? '?' + query : ''));
this.allUsers.forEach((user) => {
const entry = this.buildUser(user);
if (entry != null && !this.filterOutResult(setFilter, entry.email)) {
entries.push(entry);
}
});
return Promise.resolve(entries);
}
private buildUser(user: any) {
const entry = new UserEntry();
entry.externalId = user.id;
entry.referenceId = user.id;
entry.deleted = false;
entry.disabled = user.status === 2;
entry.email = user.email;
if (!this.validEmailAddress(entry.email) && user.username != null && user.username !== '') {
if (this.validEmailAddress(user.username)) {
entry.email = user.username;
} else if (this.syncConfig.useEmailPrefixSuffix && this.syncConfig.emailSuffix != null) {
entry.email = user.username + this.syncConfig.emailSuffix;
}
}
if (entry.email != null) {
entry.email = entry.email.trim().toLowerCase();
}
if (!this.validEmailAddress(entry.email)) {
return null;
}
return entry;
}
private async getGroups(force: boolean, setFilter: [boolean, Set<string>]): Promise<GroupEntry[]> {
const entries: GroupEntry[] = [];
const query = this.createDirectoryQuery(this.syncConfig.groupFilter);
this.logService.info('Querying groups.');
const roles = await this.apiGetMany('roles' + (query != null ? '?' + query : ''));
roles.forEach((role) => {
const entry = this.buildGroup(role);
if (entry != null && !this.filterOutResult(setFilter, entry.name)) {
entries.push(entry);
}
});
return Promise.resolve(entries);
}
private buildGroup(group: any) {
const entry = new GroupEntry();
entry.externalId = group.id;
entry.referenceId = group.id;
entry.name = group.name;
if (this.allUsers != null) {
this.allUsers.forEach((user) => {
if (user.role_id != null && user.role_id.indexOf(entry.referenceId) > -1) {
entry.userMemberExternalIds.add(user.id);
}
});
}
return entry;
}
private async getAccessToken() {
const response = await fetch(`https://api.${this.dirConfig.region}.onelogin.com/auth/oauth2/v2/token`, {
method: 'POST',
headers: new Headers({
'Authorization': 'Basic ' + btoa(this.dirConfig.clientId + ':' + this.dirConfig.clientSecret),
'Content-Type': 'application/json; charset=utf-8',
'Accept': 'application/json',
}),
body: JSON.stringify({
grant_type: 'client_credentials',
}),
});
if (response.status === 200) {
const responseJson = await response.json();
if (responseJson.access_token != null) {
return responseJson.access_token;
}
}
return null;
}
private async apiGetCall(url: string): Promise<any> {
const req: RequestInit = {
method: 'GET',
headers: new Headers({
Authorization: 'bearer:' + this.accessToken,
Accept: 'application/json',
}),
};
const response = await fetch(new Request(url, req));
if (response.status === 200) {
const responseJson = await response.json();
return responseJson;
}
return null;
}
private async apiGetMany(endpoint: string, currentData: any[] = []): Promise<any[]> {
const url = endpoint.indexOf('https://') === 0 ? endpoint :
`https://api.${this.dirConfig.region}.onelogin.com/api/1/${endpoint}`;
const response = await this.apiGetCall(url);
if (response == null || response.status == null || response.data == null) {
return currentData;
}
if (response.status.code !== 200) {
throw new Error('API call failed.');
}
currentData = currentData.concat(response.data);
if (response.pagination == null || response.pagination.next_link == null) {
return currentData;
}
return this.apiGetMany(response.pagination.next_link, currentData);
}
private validEmailAddress(email: string) {
return email != null && email !== '' && ValidEmailRegex.test(email);
}
}

View File

@@ -22,6 +22,7 @@ import { DirectoryService } from './directory.service';
import { GSuiteDirectoryService } from './gsuite-directory.service';
import { LdapDirectoryService } from './ldap-directory.service';
import { OktaDirectoryService } from './okta-directory.service';
import { OneLoginDirectoryService } from './onelogin-directory.service';
export class SyncService {
private dirType: DirectoryType;
@@ -50,7 +51,7 @@ export class SyncService {
try {
const entries = await directoryService.getEntries(force || syncConfig.overwriteExisting, test);
let groups = entries[0];
let users = entries[1];
let users = this.filterUnsupportedUsers(entries[1]);
if (groups != null && groups.length > 0) {
this.flattenUsersToGroups(groups, groups);
@@ -102,8 +103,15 @@ export class SyncService {
}
}
private filterUnsupportedUsers(users: UserEntry[]): UserEntry[] {
return users == null ? null : users.filter((u) => u.email == null || u.email.length <= 50);
}
private flattenUsersToGroups(levelGroups: GroupEntry[], allGroups: GroupEntry[]): Set<string> {
let allUsers = new Set<string>();
if (allGroups == null) {
return allUsers;
}
for (const group of levelGroups) {
const childGroups = allGroups.filter((g) => group.groupMemberReferenceIds.has(g.referenceId));
const childUsers = this.flattenUsersToGroups(childGroups, allGroups);
@@ -123,6 +131,8 @@ export class SyncService {
return new LdapDirectoryService(this.configurationService, this.logService, this.i18nService);
case DirectoryType.Okta:
return new OktaDirectoryService(this.configurationService, this.logService, this.i18nService);
case DirectoryType.OneLogin:
return new OneLoginDirectoryService(this.configurationService, this.logService, this.i18nService);
default:
return null;
}

View File

@@ -2,15 +2,9 @@ const path = require('path');
const webpack = require('webpack');
const merge = require('webpack-merge');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const AngularCompilerPlugin = require('@ngtools/webpack').AngularCompilerPlugin;
const extractCss = new ExtractTextPlugin({
filename: '[name].css',
disable: false,
allChunks: true,
});
const common = {
module: {
rules: [
@@ -53,6 +47,7 @@ const common = {
const renderer = {
mode: 'production',
devtool: false,
target: 'electron-renderer',
node: {
__dirname: false,
@@ -93,17 +88,16 @@ const renderer = {
},
{
test: /\.scss$/,
use: extractCss.extract({
use: [
{
loader: 'css-loader',
},
{
loader: 'sass-loader',
},
],
loader: MiniCssExtractPlugin.loader,
options: {
publicPath: '../',
})
},
},
'css-loader',
'sass-loader',
],
},
// Hide System.import warnings. ref: https://github.com/angular/angular/issues/21560
{
@@ -127,11 +121,13 @@ const renderer = {
chunks: ['app/vendor', 'app/main']
}),
new webpack.SourceMapDevToolPlugin({
filename: '[name].js.map',
include: ['app/main.js']
}),
new webpack.DefinePlugin({ 'global.GENTLY': false }),
extractCss,
new MiniCssExtractPlugin({
filename: '[name].[hash].css',
chunkFilename: '[id].[hash].css',
}),
],
};