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

Compare commits

..

53 Commits

Author SHA1 Message Date
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
28 changed files with 3927 additions and 2213 deletions

1
.gitignore vendored
View File

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

2
jslib

Submodule jslib updated: 8375f7381a...212a2e3745

View File

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

5352
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,7 @@
"vault", "vault",
"password manager" "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", "homepage": "https://bitwarden.com",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -22,6 +22,9 @@
"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": "npm run sub:init", "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", "rebuild": "./node_modules/.bin/electron-rebuild",
"reset": "rimraf ./node_modules/keytar/* && npm install", "reset": "rimraf ./node_modules/keytar/* && npm install",
"lint": "tslint src/**/*.ts || true", "lint": "tslint src/**/*.ts || 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\"", "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/*", "clean:dist:cli": "rimraf ./dist-cli/*",
"pack:lin": "npm run clean:dist && build --linux --x64 -p never", "pack:lin": "npm run clean:dist && electron-builder --linux --x64 -p never",
"pack:mac": "npm run clean:dist && build --mac -p never", "pack:mac": "npm run clean:dist && electron-builder --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 && electron-builder --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 && 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": "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:win": "pkg . --targets win-x64 --output ./dist-cli/windows/bwdc.exe",
"pack:cli:mac": "pkg . --targets macos-x64 --output ./dist-cli/macos/bwdc", "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: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: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", "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: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 && build --mac -p always", "publish:mac": "npm run build:dist && npm run clean:dist && electron-builder --mac -p always",
"publish:win": "npm run build:dist && npm run clean:dist && build --win --x64 --ia32 -p always -c.win.certificateSubjectName=\"8bit Solutions LLC\"" "publish:win": "npm run build:dist && npm run clean:dist && electron-builder --win --x64 --ia32 -p always -c.win.certificateSubjectName=\"8bit Solutions LLC\""
}, },
"build": { "build": {
"appId": "com.bitwarden.directory-connector", "appId": "com.bitwarden.directory-connector",
"copyright": "Copyright © 2015-2018 8bit Solutions LLC", "copyright": "Copyright © 2015-2020 Bitwarden Inc.",
"directories": { "directories": {
"buildResources": "resources", "buildResources": "resources",
"output": "dist", "output": "dist",
"app": "build" "app": "build"
}, },
"afterSign": "scripts/notarize.js",
"mac": { "mac": {
"category": "public.app-category.productivity", "category": "public.app-category.productivity",
"gatekeeperAssess": false,
"hardenedRuntime": true,
"entitlements": "resources/entitlements.mac.plist",
"entitlementsInherit": "resources/entitlements.mac.plist",
"target": [ "target": [
"dmg", "dmg",
"zip" "zip"
@@ -137,7 +145,7 @@
"@types/inquirer": "^0.0.43", "@types/inquirer": "^0.0.43",
"@types/ldapjs": "^1.0.3", "@types/ldapjs": "^1.0.3",
"@types/lowdb": "^1.0.5", "@types/lowdb": "^1.0.5",
"@types/lunr": "^2.1.6", "@types/lunr": "^2.3.3",
"@types/node": "^10.9.4", "@types/node": "^10.9.4",
"@types/node-fetch": "^2.1.2", "@types/node-fetch": "^2.1.2",
"@types/node-forge": "^0.7.5", "@types/node-forge": "^0.7.5",
@@ -153,20 +161,21 @@
"cross-env": "^5.2.0", "cross-env": "^5.2.0",
"css-loader": "^1.0.0", "css-loader": "^1.0.0",
"del": "^3.0.0", "del": "^3.0.0",
"electron": "3.0.14", "electron": "6.1.7",
"electron-builder": "20.38.5", "electron-builder": "22.4.0",
"electron-rebuild": "^1.8.2", "electron-notarize": "^0.2.1",
"electron-reload": "^1.4.0", "electron-rebuild": "^1.9.0",
"extract-text-webpack-plugin": "next", "electron-reload": "^1.5.0",
"mini-css-extract-plugin": "^0.9.0",
"file-loader": "^2.0.0", "file-loader": "^2.0.0",
"font-awesome": "4.7.0", "font-awesome": "4.7.0",
"gulp": "^4.0.0", "gulp": "^4.0.0",
"gulp-google-webfonts": "^2.0.0", "gulp-google-webfonts": "^2.0.0",
"html-loader": "^0.5.5", "html-loader": "^0.5.5",
"html-webpack-plugin": "^3.2.0", "html-webpack-plugin": "^3.2.0",
"node-abi": "^2.5.1", "node-abi": "^2.9.0",
"node-loader": "^0.6.0", "node-loader": "^0.6.0",
"node-sass": "^4.11.0", "node-sass": "^4.13.1",
"pkg": "4.3.4", "pkg": "4.3.4",
"rimraf": "^2.6.2", "rimraf": "^2.6.2",
"sass-loader": "^7.1.0", "sass-loader": "^7.1.0",
@@ -185,13 +194,12 @@
"@angular/compiler": "7.2.1", "@angular/compiler": "7.2.1",
"@angular/core": "7.2.1", "@angular/core": "7.2.1",
"@angular/forms": "7.2.1", "@angular/forms": "7.2.1",
"@angular/http": "7.2.1",
"@angular/platform-browser": "7.2.1", "@angular/platform-browser": "7.2.1",
"@angular/platform-browser-dynamic": "7.2.1", "@angular/platform-browser-dynamic": "7.2.1",
"@angular/router": "7.2.1", "@angular/router": "7.2.1",
"@angular/upgrade": "7.2.1", "@angular/upgrade": "7.2.1",
"@microsoft/microsoft-graph-client": "1.2.0", "@microsoft/microsoft-graph-client": "1.2.0",
"@okta/okta-sdk-nodejs": "1.2.0", "@okta/okta-sdk-nodejs": "2.0.1",
"angular2-toaster": "6.1.0", "angular2-toaster": "6.1.0",
"angulartics2": "6.3.0", "angulartics2": "6.3.0",
"big-integer": "1.6.36", "big-integer": "1.6.36",
@@ -202,11 +210,12 @@
"duo_web_sdk": "git+https://github.com/duosecurity/duo_web_sdk.git", "duo_web_sdk": "git+https://github.com/duosecurity/duo_web_sdk.git",
"electron-log": "2.2.17", "electron-log": "2.2.17",
"electron-store": "1.3.0", "electron-store": "1.3.0",
"electron-updater": "4.0.6", "electron-updater": "4.2.0",
"form-data": "2.3.2", "form-data": "2.3.2",
"googleapis": "33.0.0", "googleapis": "43.0.0",
"https-proxy-agent": "4.0.0",
"inquirer": "6.2.0", "inquirer": "6.2.0",
"keytar": "4.4.1", "keytar": "4.13.0",
"ldapjs": "git+https://git@github.com/kspearrin/node-ldapjs.git", "ldapjs": "git+https://git@github.com/kspearrin/node-ldapjs.git",
"lowdb": "1.0.0", "lowdb": "1.0.0",
"lunr": "2.3.3", "lunr": "2.3.3",

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>

17
scripts/notarize.js Normal file
View File

@@ -0,0 +1,17 @@
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.APPLEID;
const appName = context.packager.appInfo.productFilename;
return await notarize({
appBundleId: 'com.bitwarden.directory-connector',
appPath: `${appOutDir}/${appName}.app`,
appleId: appleId,
appleIdPassword: `@keychain:AC_PASSWORD`,
});
};

View File

@@ -11,6 +11,7 @@ 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 { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service'; import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { StateService } from 'jslib/abstractions/state.service';
import { StorageService } from 'jslib/abstractions/storage.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';
@@ -25,8 +26,9 @@ export class LoginComponent extends BaseLoginComponent {
constructor(authService: AuthService, router: Router, constructor(authService: AuthService, router: Router,
i18nService: I18nService, private componentFactoryResolver: ComponentFactoryResolver, i18nService: I18nService, private componentFactoryResolver: ComponentFactoryResolver,
storageService: StorageService, platformUtilsService: PlatformUtilsService) { storageService: StorageService, platformUtilsService: PlatformUtilsService,
super(authService, router, platformUtilsService, i18nService, storageService); stateService: StateService) {
super(authService, router, platformUtilsService, i18nService, storageService, stateService);
super.successRoute = '/tabs/dashboard'; super.successRoute = '/tabs/dashboard';
} }

View File

@@ -16,6 +16,8 @@ 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 { StateService } from 'jslib/abstractions/state.service';
import { StorageService } from 'jslib/abstractions/storage.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';
@@ -30,8 +32,10 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
constructor(authService: AuthService, router: Router, constructor(authService: AuthService, router: Router,
i18nService: I18nService, apiService: ApiService, i18nService: I18nService, apiService: ApiService,
platformUtilsService: PlatformUtilsService, environmentService: EnvironmentService, platformUtilsService: PlatformUtilsService, environmentService: EnvironmentService,
private componentFactoryResolver: ComponentFactoryResolver) { private componentFactoryResolver: ComponentFactoryResolver, stateService: StateService,
super(authService, router, i18nService, apiService, platformUtilsService, window, environmentService); storageService: StorageService) {
super(authService, router, i18nService, apiService, platformUtilsService, window, environmentService,
stateService, storageService);
super.successRoute = '/tabs/dashboard'; super.successRoute = '/tabs/dashboard';
} }

View File

@@ -15,6 +15,7 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
import { CalloutComponent } from 'jslib/angular/components/callout.component';
import { IconComponent } from 'jslib/angular/components/icon.component'; import { IconComponent } from 'jslib/angular/components/icon.component';
import { ModalComponent } from 'jslib/angular/components/modal.component'; import { ModalComponent } from 'jslib/angular/components/modal.component';
@@ -60,6 +61,7 @@ import { SearchCiphersPipe } from 'jslib/angular/pipes/search-ciphers.pipe';
AutofocusDirective, AutofocusDirective,
BlurClickDirective, BlurClickDirective,
BoxRowDirective, BoxRowDirective,
CalloutComponent,
DashboardComponent, DashboardComponent,
EnvironmentComponent, EnvironmentComponent,
FallbackSrcDirective, FallbackSrcDirective,

View File

@@ -6,7 +6,7 @@
<p> <p>
{{'bitwardenDirectoryConnector' | i18n}} {{'bitwardenDirectoryConnector' | i18n}}
<br /> {{'version' | i18n : version}} <br /> {{'version' | i18n : version}}
<br /> &copy; 8bit Solutions LLC 2015-{{year}} <br /> &copy; Bitwarden Inc. LLC 2015-{{year}}
</p> </p>
<button class="btn btn-primary" type="button" (click)="update()" [disabled]="checkingForUpdate"> <button class="btn btn-primary" type="button" (click)="update()" [disabled]="checkingForUpdate">
<i class="fa fa-download fa-fw" [hidden]="checkingForUpdate"></i> <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> <label class="form-check-label" for="ad">{{'ldapAd' | i18n}}</label>
</div> </div>
</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-group">
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="ssl" [(ngModel)]="ldap.ssl" name="SSL"> <input class="form-check-input" type="checkbox" id="ldapEncrypted" [(ngModel)]="ldap.ssl"
<label class="form-check-label" for="ssl">{{'ldapSsl' | i18n}}</label> name="Encrypted">
<label class="form-check-label" for="ldapEncrypted">{{'ldapEncrypted' | i18n}}</label>
</div> </div>
</div> </div>
<div class="ml-4" *ngIf="ldap.ssl"> <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> <p>{{'ldapSslUntrustedDesc' | i18n}}</p>
<div class="form-group"> <div class="form-group">
<label for="sslCertPath">{{'ldapSslCert' | i18n}}</label> <label for="sslCertPath">{{'ldapSslCert' | i18n}}</label>
@@ -62,12 +94,13 @@
<input type="text" class="form-control" id="sslCaPath" name="SSLCaPath" <input type="text" class="form-control" id="sslCaPath" name="SSLCaPath"
[(ngModel)]="ldap.sslCaPath"> [(ngModel)]="ldap.sslCaPath">
</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="sslAllowUnauthorized" <input class="form-check-input" type="checkbox" id="certDoNotVerify"
[(ngModel)]="ldap.sslAllowUnauthorized" name="SSLAllowUnauthorized"> [(ngModel)]="ldap.sslAllowUnauthorized" name="CertDoNoVerify">
<label class="form-check-label" <label class="form-check-label"
for="sslAllowUnauthorized">{{'ldapSslAllowUnauthorized' | i18n}}</label> for="certDoNotVerify">{{'ldapCertDoNotVerify' | i18n}}</label>
</div> </div>
</div> </div>
</div> </div>
@@ -122,6 +155,26 @@
[(ngModel)]="okta.token"> [(ngModel)]="okta.token">
</div> </div>
</div> </div>
<div [hidden]="directory != directoryType.OneLogin">
<div class="form-group">
<label for="oneLoginClientId">{{'clientId' | i18n}}</label>
<input type="text" class="form-control" id="oneLoginClientId" name="OneLoginClientId"
[(ngModel)]="oneLogin.clientId">
</div>
<div class="form-group">
<label for="oneLoginClientSecret">{{'clientSecret' | i18n}}</label>
<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 [hidden]="directory != directoryType.GSuite">
<div class="form-group"> <div class="form-group">
<label for="domain">{{'domain' | i18n}}</label> <label for="domain">{{'domain' | i18n}}</label>

View File

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

View File

@@ -25,6 +25,8 @@ import { NoopMessagingService } from 'jslib/services/noopMessaging.service';
import { TokenService } from 'jslib/services/token.service'; import { TokenService } from 'jslib/services/token.service';
import { UserService } from 'jslib/services/user.service'; import { UserService } from 'jslib/services/user.service';
import { StorageService as StorageServiceAbstraction } from 'jslib/abstractions/storage.service';
import { Program } from './program'; import { Program } from './program';
// tslint:disable-next-line // tslint:disable-next-line
@@ -35,7 +37,7 @@ export class Main {
logService: ConsoleLogService; logService: ConsoleLogService;
messagingService: NoopMessagingService; messagingService: NoopMessagingService;
storageService: LowdbStorageService; storageService: LowdbStorageService;
secureStorageService: KeytarSecureStorageService; secureStorageService: StorageServiceAbstraction;
i18nService: I18nService; i18nService: I18nService;
platformUtilsService: CliPlatformUtilsService; platformUtilsService: CliPlatformUtilsService;
constantsService: ConstantsService; constantsService: ConstantsService;
@@ -70,13 +72,15 @@ export class Main {
this.dataFilePath = path.join(process.env.HOME, '.config/' + applicationName); 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.i18nService = new I18nService('en', './locales');
this.platformUtilsService = new CliPlatformUtilsService('connector', packageJson); this.platformUtilsService = new CliPlatformUtilsService('connector', packageJson);
this.logService = new ConsoleLogService(this.platformUtilsService.isDev(), 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.cryptoFunctionService = new NodeCryptoFunctionService();
this.storageService = new LowdbStorageService(null, this.dataFilePath, true); this.storageService = new LowdbStorageService(null, this.dataFilePath, true);
this.secureStorageService = new KeytarSecureStorageService(applicationName); this.secureStorageService = plaintextSecrets ?
this.storageService : new KeytarSecureStorageService(applicationName);
this.cryptoService = new CryptoService(this.storageService, this.secureStorageService, this.cryptoService = new CryptoService(this.storageService, this.secureStorageService,
this.cryptoFunctionService); this.cryptoFunctionService);
this.appIdService = new AppIdService(this.storageService); this.appIdService = new AppIdService(this.storageService);
@@ -89,7 +93,8 @@ export class Main {
this.containerService = new ContainerService(this.cryptoService); this.containerService = new ContainerService(this.cryptoService);
this.authService = new AuthService(this.cryptoService, this.apiService, this.userService, this.tokenService, this.authService = new AuthService(this.cryptoService, this.apiService, this.userService, this.tokenService,
this.appIdService, this.i18nService, this.platformUtilsService, this.messagingService, true); this.appIdService, this.i18nService, this.platformUtilsService, this.messagingService, true);
this.configurationService = new ConfigurationService(this.storageService, this.secureStorageService); 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.syncService = new SyncService(this.configurationService, this.logService, this.cryptoFunctionService,
this.apiService, this.messagingService, this.i18nService); this.apiService, this.messagingService, this.i18nService);
this.program = new Program(this); this.program = new Program(this);

View File

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

View File

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

View File

@@ -414,8 +414,20 @@
"sync": { "sync": {
"message": "Sync" "message": "Sync"
}, },
"ldapEncrypted": {
"message": "This server uses an encrypted connection"
},
"ldapTls": {
"message": "Use TLS (STARTTLS)"
},
"ldapTlsCa": {
"message": "Certificate CA Chain (PEM)"
},
"ldapSsl": { "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": { "ldapSslUntrustedDesc": {
"message": "If your LDAPS server uses an untrusted certificate you can configure certificate options below." "message": "If your LDAPS server uses an untrusted certificate you can configure certificate options below."
@@ -429,12 +441,15 @@
"ldapSslKey": { "ldapSslKey": {
"message": "Certificate Private Key (PEM)" "message": "Certificate Private Key (PEM)"
}, },
"ldapSslAllowUnauthorized": { "ldapCertDoNotVerify": {
"message": "Allow untrusted SSL connections (not recommended)." "message": "Do not verify server certificates (not recommended)."
}, },
"ldapAd": { "ldapAd": {
"message": "This server uses Active Directory" "message": "This server uses Active Directory"
}, },
"ldapPagedResults": {
"message": "This server pages search results"
},
"select": { "select": {
"message": "Select" "message": "Select"
}, },
@@ -587,5 +602,14 @@
}, },
"overwriteExisting": { "overwriteExisting": {
"message": "Overwrite existing organization users based on current sync settings." "message": "Overwrite existing organization users based on current sync settings."
},
"clientId": {
"message": "Client ID"
},
"clientSecret": {
"message": "Client Secret"
},
"region": {
"message": "Region"
} }
} }

View File

@@ -1,5 +1,7 @@
export class LdapConfiguration { export class LdapConfiguration {
ssl = false; ssl = false;
startTls = false;
tlsCaPath: string;
sslAllowUnauthorized = false; sslAllowUnauthorized = false;
sslCertPath: string; sslCertPath: string;
sslKeyPath: string; sslKeyPath: string;
@@ -12,4 +14,5 @@ export class LdapConfiguration {
username: string; username: string;
password: string; password: string;
ad = true; 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", "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.6.1", "version": "2.7.0",
"author": "8bit Solutions LLC <hello@bitwarden.com> (https://bitwarden.com)", "author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)",
"homepage": "https://bitwarden.com", "homepage": "https://bitwarden.com",
"license": "GPL-3.0", "license": "GPL-3.0",
"main": "main.js", "main": "main.js",
@@ -14,7 +14,7 @@
"dependencies": { "dependencies": {
"electron-log": "2.2.17", "electron-log": "2.2.17",
"electron-store": "1.3.0", "electron-store": "1.3.0",
"electron-updater": "4.0.6", "electron-updater": "4.2.0",
"keytar": "4.4.1" "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('--raw', 'Return raw output instead of a descriptive message.')
.option('--response', 'Return a JSON formatted version of response output.') .option('--response', 'Return a JSON formatted version of response output.')
.option('--quiet', 'Don\'t return anything to stdout.') .option('--quiet', 'Don\'t return anything to stdout.')
.option('--nointeraction', 'Do not prompt for interactive user input.')
.version(this.main.platformUtilsService.getApplicationVersion(), '-v, --version'); .version(this.main.platformUtilsService.getApplicationVersion(), '-v, --version');
program.on('option:pretty', () => { program.on('option:pretty', () => {
@@ -58,6 +59,10 @@ export class Program extends BaseProgram {
process.env.BW_RESPONSE = 'true'; process.env.BW_RESPONSE = 'true';
}); });
program.on('option:nointeraction', () => {
process.env.BW_NOINTERACTION = 'true';
});
program.on('command:*', () => { program.on('command:*', () => {
writeLn(chalk.redBright('Invalid command: ' + program.args.join(' ')), false, true); writeLn(chalk.redBright('Invalid command: ' + program.args.join(' ')), false, true);
writeLn('See --help for a list of available commands.', true, true); writeLn('See --help for a list of available commands.', true, true);
@@ -184,6 +189,7 @@ export class Program extends BaseProgram {
writeLn(' azure.key - The Azure AD secret key.'); writeLn(' azure.key - The Azure AD secret key.');
writeLn(' gsuite.key - The G Suite private key.'); writeLn(' gsuite.key - The G Suite private key.');
writeLn(' okta.token - The Okta token.'); writeLn(' okta.token - The Okta token.');
writeLn(' onelogin.secret - The OneLogin client secret.');
writeLn(''); writeLn('');
writeLn(' Examples:'); writeLn(' Examples:');
writeLn(''); writeLn('');
@@ -194,6 +200,7 @@ export class Program extends BaseProgram {
writeLn(' bwdc config azure.key <key>'); writeLn(' bwdc config azure.key <key>');
writeLn(' bwdc config gsuite.key <key>'); writeLn(' bwdc config gsuite.key <key>');
writeLn(' bwdc config okta.token <token>'); writeLn(' bwdc config okta.token <token>');
writeLn(' bwdc config onelogin.secret <secret>');
writeLn('', true); writeLn('', true);
}) })
.action(async (setting, value, cmd) => { .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);
$font-family-sans-serif: 'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; $font-family-sans-serif: 'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
$h1-font-size: 2rem; $h1-font-size: 2rem;

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,176 @@
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';
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);
}
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.email = user.email != null ? user.email.trim().toLowerCase() : null;
entry.deleted = false;
entry.disabled = user.status === 2;
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);
}
}

View File

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

View File

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