1
0
mirror of https://github.com/bitwarden/directory-connector synced 2025-12-17 00:33:35 +00:00

Compare commits

..

61 Commits

Author SHA1 Message Date
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
Kyle Spearrin
2617f96710 bump version 2019-06-08 13:30:09 -04:00
Kyle Spearrin
53b0614faf only set domain if it has value 2019-06-08 00:15:44 -04:00
Kyle Spearrin
a01db9c448 empty string check on customer 2019-06-08 00:11:29 -04:00
Kyle Spearrin
84dc8e3696 update jslib 2019-06-07 11:22:26 -04:00
Kyle Spearrin
e7b988c042 trimLeft on gsuite key 2019-06-07 11:21:55 -04:00
Kyle Spearrin
a06212af1a write failed responses to stderr 2019-06-04 21:03:30 -04:00
Kyle Spearrin
b217ac9456 update jslib 2019-06-04 00:09:40 -04:00
Kyle Spearrin
ff8422d6c1 alwaysOnTop 2019-06-03 08:41:19 -04:00
Kyle Spearrin
b51d279ba6 menu bar on mac 2019-06-01 22:15:24 -04:00
Kyle Spearrin
78e4601413 update jslib 2019-05-27 10:31:22 -04:00
Kyle Spearrin
5900db10eb update jslib 2019-05-13 08:54:32 -04:00
Kyle Spearrin
c9bd853032 version bump 2019-05-06 21:32:56 -04:00
Kyle Spearrin
976fcad098 overwriteExisting on org user import 2019-05-06 21:32:25 -04:00
Kyle Spearrin
5d8193c348 update jslib 2019-04-18 10:11:38 -04:00
Kyle Spearrin
cc09b26818 hideTitleBar false 2019-04-13 21:37:54 -04:00
Kyle Spearrin
b0247caa4c getDeletedUsers for azure ad 2019-04-12 09:58:19 -04:00
Kyle Spearrin
38316abeae premises 2019-04-04 00:45:46 -04:00
Kyle Spearrin
0991dfa6bd update jslib 2019-04-02 11:54:07 -04:00
Kyle Spearrin
88029663e9 update jslib 2019-04-02 08:20:10 -04:00
Kyle Spearrin
3f5cb10db6 ignore add-edit component 2019-03-29 00:36:07 -04:00
Kyle Spearrin
1c927722f0 update bootstrap 2019-03-29 00:25:06 -04:00
Kyle Spearrin
c04354ba1c npm audit fix 2019-03-29 00:06:07 -04:00
Kyle Spearrin
3335659c8d bump version 2019-03-26 12:24:16 -04:00
Kyle Spearrin
f0282b33f0 add paging to members list api 2019-03-26 09:06:42 -04:00
Kyle Spearrin
f69d461463 move scope of filters for delUsers query 2019-03-26 09:02:35 -04:00
Elie Mélois
bcf16562fe Manage multiple pages when fetching users (#15) 2019-03-26 08:57:01 -04:00
Kyle Spearrin
d7412410f2 group paging cleanup 2019-03-26 08:02:09 -04:00
Elie Mélois
d5907477df Manage multiple pages when fetching groups (#14) 2019-03-26 07:58:05 -04:00
Kyle Spearrin
9acfaff361 remove echo 2019-03-22 22:25:37 -04:00
Kyle Spearrin
737274da59 test for ver info during install 2019-03-22 22:20:41 -04:00
Kyle Spearrin
7a0e0e6915 WIN_PKG set with ps 2019-03-22 22:16:00 -04:00
Kyle Spearrin
25847b9c83 ver info testing 2019-03-22 22:11:32 -04:00
Kyle Spearrin
07dd7bb93e bump version 2019-03-22 17:35:29 -04:00
Kyle Spearrin
bc9936bd92 update lock file 2019-03-22 17:29:38 -04:00
Kyle Spearrin
e4428e3387 update ldapjs fork 2019-03-22 17:21:18 -04:00
Kyle Spearrin
30ef0ce140 back to node 10 2019-03-22 16:55:51 -04:00
Kyle Spearrin
38c461ebe0 use ldapjs fork 2019-03-22 16:55:20 -04:00
Kyle Spearrin
ead1776f90 try node 9 2019-03-22 16:38:49 -04:00
Kyle Spearrin
0e944b2442 simplify sim promise 2019-03-22 13:12:52 -04:00
Kyle Spearrin
3bbd503308 RELEASE_NAME is just numbers 2019-03-20 23:09:54 -04:00
Kyle Spearrin
a7b13b168e release name fix 2019-03-20 23:09:12 -04:00
Kyle Spearrin
1cf8ae1bdf bump version 2019-03-20 22:16:31 -04:00
Kyle Spearrin
7da3c8e252 Merge branch 'master' of github.com:bitwarden/directory-connector 2019-03-20 17:33:13 -04:00
Kyle Spearrin
2edf3fb68d data-file and last-sync commands 2019-03-20 17:33:11 -04:00
Kyle Spearrin
1e698ba81b Update README.md 2019-03-20 16:44:41 -04:00
Kyle Spearrin
e9034ea6fe Update README.md 2019-03-20 16:43:48 -04:00
28 changed files with 2639 additions and 1473 deletions

1
.gitignore vendored
View File

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

View File

@@ -18,7 +18,32 @@ The application is written using Electron with Angular and installs on Windows,
![Directory Connector](https://raw.githubusercontent.com/bitwarden/brand/master/screenshots/directory-connector-macos.png "Dashboard") ![Directory Connector](https://raw.githubusercontent.com/bitwarden/brand/master/screenshots/directory-connector-macos.png "Dashboard")
# Build/Run ## Command-line Interface
A command-line interface tool is also available for the Bitwarden Directory Connector. The Directory Connector CLI (`bwdc`) is written with TypeScript and Node.js and can also be run on Windows, macOS, and Linux distributions.
## CLI Documentation
The Bitwarden Directory Connector CLI is self-documented with `--help` content and examples for every command. You should start exploring the CLI by using the global `--help` option:
```bash
bwdc --help
```
This option will list all available commands that you can use with the Directory Connector CLI.
Additionally, you can run the `--help` option on a specific command to learn more about it:
```
bwdc test --help
bwdc config --help
```
**Detailed Documentation**
We provide detailed documentation and examples for using the Directory Connector CLI in our help center at https://help.bitwarden.com/article/directory-sync/#command-line-interface.
## Build/Run
**Requirements** **Requirements**
@@ -48,7 +73,7 @@ You can then run commands from the `./build-cli` folder:
node ./build-cli/bwdc.js --help node ./build-cli/bwdc.js --help
``` ```
# Contribute ## Contribute
Code contributions are welcome! Please commit any pull requests against the `master` branch. Learn more about how to contribute by reading the [`CONTRIBUTING.md`](CONTRIBUTING.md) file. Code contributions are welcome! Please commit any pull requests against the `master` branch. Learn more about how to contribute by reading the [`CONTRIBUTING.md`](CONTRIBUTING.md) file.

View File

@@ -24,21 +24,23 @@ init:
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Install-Product node 10 Install-Product node 10
$env:PATH = "C:\Program Files (x86)\Resource Hacker;${env:PATH}" $env:PATH = "C:\Program Files (x86)\Resource Hacker;${env:PATH}"
if(Test-Path -Path $env:WIN_PKG) { }
$env:VER_INFO = "true" if($env:APPVEYOR_REPO_TAG -eq "true") {
} $env:RELEASE_NAME = $env:APPVEYOR_REPO_TAG_NAME.TrimStart("v")
} }
install: install:
- ps: | - ps: |
$env:PACKAGE_VERSION = (Get-Content -Raw -Path .\src\package.json | ConvertFrom-Json).version $env:PACKAGE_VERSION = (Get-Content -Raw -Path .\src\package.json | ConvertFrom-Json).version
$env:RELEASE_NAME = "Version ${tagName}"
$env:PROD_DEPLOY = "false" $env:PROD_DEPLOY = "false"
if($env:APPVEYOR_REPO_TAG -eq "true" -and $env:APPVEYOR_RE_BUILD -eq "True") { if($env:APPVEYOR_REPO_TAG -eq "true" -and $env:APPVEYOR_RE_BUILD -eq "True") {
$env:PROD_DEPLOY = "true" $env:PROD_DEPLOY = "true"
echo "This is a production deployment." echo "This is a production deployment."
} }
if($isWindows) { if($isWindows) {
if(Test-Path -Path $env:WIN_PKG) {
$env:VER_INFO = "true"
}
choco install reshack --no-progress choco install reshack --no-progress
choco install cloc --no-progress choco install cloc --no-progress
choco install checksum --no-progress choco install checksum --no-progress
@@ -136,17 +138,17 @@ for:
only: only:
- image: Visual Studio 2017 - image: Visual Studio 2017
cache: cache:
- '%LOCALAPPDATA%\electron -> appveyor.yml' - '%LOCALAPPDATA%\electron'
- '%LOCALAPPDATA%\electron-builder -> appveyor.yml' - '%LOCALAPPDATA%\electron-builder'
- 'C:\Users\appveyor\.pkg-cache\ -> package.json' - 'C:\Users\appveyor\.pkg-cache\'
- -
matrix: matrix:
only: only:
- image: Ubuntu1804 - image: Ubuntu1804
cache: cache:
- '/home/appveyor/.cache/electron -> appveyor.yml' - '/home/appveyor/.cache/electron'
- '/home/appveyor/.cache/electron-builder -> appveyor.yml' - '/home/appveyor/.cache/electron-builder'
deploy: deploy:
tag: $(APPVEYOR_REPO_TAG_NAME) tag: $(APPVEYOR_REPO_TAG_NAME)

2
jslib

Submodule jslib updated: 50e6f24679...b74ee7b3ee

3433
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -22,6 +22,7 @@
"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",
"simlink:win": "rm -rf ./jslib && cmd /c mklink /J .\\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 +39,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,9 +55,9 @@
"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",
@@ -66,8 +67,13 @@
"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"
@@ -129,7 +135,7 @@
"assets": "./build-cli/**/*" "assets": "./build-cli/**/*"
}, },
"devDependencies": { "devDependencies": {
"@angular/compiler-cli": "^7.2.1", "@angular/compiler-cli": "^7.2.11",
"@microsoft/microsoft-graph-types": "^1.4.0", "@microsoft/microsoft-graph-types": "^1.4.0",
"@ngtools/webpack": "^7.2.2", "@ngtools/webpack": "^7.2.2",
"@types/commander": "^2.12.2", "@types/commander": "^2.12.2",
@@ -153,10 +159,11 @@
"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": "5.0.8",
"electron-builder": "20.38.5", "electron-builder": "21.1.5",
"electron-rebuild": "^1.8.2", "electron-notarize": "^0.1.1",
"electron-reload": "^1.4.0", "electron-rebuild": "^1.8.5",
"electron-reload": "^1.4.1",
"extract-text-webpack-plugin": "next", "extract-text-webpack-plugin": "next",
"file-loader": "^2.0.0", "file-loader": "^2.0.0",
"font-awesome": "4.7.0", "font-awesome": "4.7.0",
@@ -164,9 +171,9 @@
"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.9.3", "node-sass": "^4.11.0",
"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",
@@ -195,19 +202,20 @@
"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",
"bootstrap": "4.1.3", "bootstrap": "4.3.1",
"chalk": "2.4.1", "chalk": "2.4.1",
"commander": "2.18.0", "commander": "2.18.0",
"core-js": "2.6.2", "core-js": "2.6.2",
"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.1.2",
"form-data": "2.3.2", "form-data": "2.3.2",
"googleapis": "33.0.0", "googleapis": "33.0.0",
"https-proxy-agent": "2.2.1",
"inquirer": "6.2.0", "inquirer": "6.2.0",
"keytar": "4.4.1", "keytar": "4.13.0",
"ldapjs": "1.0.2", "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",
"node-fetch": "2.2.0", "node-fetch": "2.2.0",

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

@@ -27,6 +27,7 @@ import { MoreComponent } from './tabs/more.component';
import { SettingsComponent } from './tabs/settings.component'; import { SettingsComponent } from './tabs/settings.component';
import { TabsComponent } from './tabs/tabs.component'; import { TabsComponent } from './tabs/tabs.component';
import { A11yTitleDirective } from 'jslib/angular/directives/a11y-title.directive';
import { ApiActionDirective } from 'jslib/angular/directives/api-action.directive'; import { ApiActionDirective } from 'jslib/angular/directives/api-action.directive';
import { AutofocusDirective } from 'jslib/angular/directives/autofocus.directive'; import { AutofocusDirective } from 'jslib/angular/directives/autofocus.directive';
import { BlurClickDirective } from 'jslib/angular/directives/blur-click.directive'; import { BlurClickDirective } from 'jslib/angular/directives/blur-click.directive';
@@ -53,6 +54,7 @@ import { SearchCiphersPipe } from 'jslib/angular/pipes/search-ciphers.pipe';
ToasterModule.forRoot(), ToasterModule.forRoot(),
], ],
declarations: [ declarations: [
A11yTitleDirective,
ApiActionDirective, ApiActionDirective,
AppComponent, AppComponent,
AutofocusDirective, AutofocusDirective,

View File

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

View File

@@ -15,6 +15,7 @@ import { StateService } from 'jslib/abstractions/state.service';
import { SyncService } from '../../services/sync.service'; import { SyncService } from '../../services/sync.service';
import { GroupEntry } from '../../models/groupEntry'; import { GroupEntry } from '../../models/groupEntry';
import { SimResult } from '../../models/simResult';
import { UserEntry } from '../../models/userEntry'; import { UserEntry } from '../../models/userEntry';
import { ConfigurationService } from '../../services/configuration.service'; import { ConfigurationService } from '../../services/configuration.service';
@@ -34,7 +35,7 @@ export class DashboardComponent implements OnInit, OnDestroy {
simEnabledUsers: UserEntry[] = []; simEnabledUsers: UserEntry[] = [];
simDisabledUsers: UserEntry[] = []; simDisabledUsers: UserEntry[] = [];
simDeletedUsers: UserEntry[] = []; simDeletedUsers: UserEntry[] = [];
simPromise: Promise<any>; simPromise: Promise<SimResult>;
simSinceLast: boolean = false; simSinceLast: boolean = false;
syncPromise: Promise<[GroupEntry[], UserEntry[]]>; syncPromise: Promise<[GroupEntry[], UserEntry[]]>;
startPromise: Promise<any>; startPromise: Promise<any>;
@@ -101,22 +102,18 @@ export class DashboardComponent implements OnInit, OnDestroy {
this.simDisabledUsers = []; this.simDisabledUsers = [];
this.simDeletedUsers = []; this.simDeletedUsers = [];
this.simPromise = new Promise(async (resolve, reject) => { try {
try { this.simPromise = ConnectorUtils.simulate(this.syncService, this.i18nService, this.simSinceLast);
const result = await ConnectorUtils.simulate(this.syncService, this.i18nService, this.simSinceLast); const result = await this.simPromise;
this.simGroups = result.groups; this.simGroups = result.groups;
this.simUsers = result.users; this.simUsers = result.users;
this.simEnabledUsers = result.enabledUsers; this.simEnabledUsers = result.enabledUsers;
this.simDisabledUsers = result.disabledUsers; this.simDisabledUsers = result.disabledUsers;
this.simDeletedUsers = result.deletedUsers; this.simDeletedUsers = result.deletedUsers;
} catch (e) { } catch (e) {
this.simGroups = null; this.simGroups = null;
this.simUsers = null; this.simUsers = null;
reject(e); }
return;
}
resolve();
});
} }
private async updateLastSync() { private async updateLastSync() {

View File

@@ -190,6 +190,13 @@
<label class="form-check-label" for="removeDisabled">{{'removeDisabled' | i18n}}</label> <label class="form-check-label" for="removeDisabled">{{'removeDisabled' | i18n}}</label>
</div> </div>
</div> </div>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="overwriteExisting"
[(ngModel)]="sync.overwriteExisting" name="OverwriteExisting">
<label class="form-check-label" for="overwriteExisting">{{'overwriteExisting' | i18n}}</label>
</div>
</div>
<div [hidden]="directory != directoryType.Ldap"> <div [hidden]="directory != directoryType.Ldap">
<div [hidden]="ldap.ad"> <div [hidden]="ldap.ad">
<div class="form-group"> <div class="form-group">

View File

@@ -25,16 +25,19 @@ 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
const packageJson = require('./package.json'); const packageJson = require('./package.json');
export class Main { export class Main {
dataFilePath: string;
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;
@@ -53,30 +56,31 @@ export class Main {
constructor() { constructor() {
const applicationName = 'Bitwarden Directory Connector'; const applicationName = 'Bitwarden Directory Connector';
let p = null;
if (process.env.BITWARDENCLI_CONNECTOR_APPDATA_DIR) { if (process.env.BITWARDENCLI_CONNECTOR_APPDATA_DIR) {
p = path.resolve(process.env.BITWARDENCLI_CONNECTOR_APPDATA_DIR); this.dataFilePath = path.resolve(process.env.BITWARDENCLI_CONNECTOR_APPDATA_DIR);
} else if (process.env.BITWARDEN_CONNECTOR_APPDATA_DIR) { } else if (process.env.BITWARDEN_CONNECTOR_APPDATA_DIR) {
p = path.resolve(process.env.BITWARDEN_CONNECTOR_APPDATA_DIR); this.dataFilePath = path.resolve(process.env.BITWARDEN_CONNECTOR_APPDATA_DIR);
} else if (fs.existsSync(path.join(__dirname, 'bitwarden-connector-appdata'))) { } else if (fs.existsSync(path.join(__dirname, 'bitwarden-connector-appdata'))) {
p = path.join(__dirname, 'bitwarden-connector-appdata'); this.dataFilePath = path.join(__dirname, 'bitwarden-connector-appdata');
} else if (process.platform === 'darwin') { } else if (process.platform === 'darwin') {
p = path.join(process.env.HOME, 'Library/Application Support/' + applicationName); this.dataFilePath = path.join(process.env.HOME, 'Library/Application Support/' + applicationName);
} else if (process.platform === 'win32') { } else if (process.platform === 'win32') {
p = path.join(process.env.APPDATA, applicationName); this.dataFilePath = path.join(process.env.APPDATA, applicationName);
} else if (process.env.XDG_CONFIG_HOME) { } else if (process.env.XDG_CONFIG_HOME) {
p = path.join(process.env.XDG_CONFIG_HOME, applicationName); this.dataFilePath = path.join(process.env.XDG_CONFIG_HOME, applicationName);
} else { } else {
p = 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, p, 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

@@ -86,7 +86,7 @@ export class ConfigCommand {
private async setGSuiteKey(key: string) { private async setGSuiteKey(key: string) {
await this.loadConfig(); await this.loadConfig();
this.gsuite.privateKey = key; this.gsuite.privateKey = key != null ? key.trimLeft() : null;
await this.saveConfig(); await this.saveConfig();
} }

View File

@@ -0,0 +1,29 @@
import * as program from 'commander';
import { ConfigurationService } from '../services/configuration.service';
import { Response } from 'jslib/cli/models/response';
import { StringResponse } from 'jslib/cli/models/response/stringResponse';
export class LastSyncCommand {
constructor(private configurationService: ConfigurationService) { }
async run(object: string, cmd: program.Command): Promise<Response> {
try {
switch (object.toLowerCase()) {
case 'groups':
const groupsDate = await this.configurationService.getLastGroupSyncDate();
const groupsRes = new StringResponse(groupsDate == null ? null : groupsDate.toISOString());
return Response.success(groupsRes);
case 'users':
const usersDate = await this.configurationService.getLastUserSyncDate();
const usersRes = new StringResponse(usersDate == null ? null : usersDate.toISOString());
return Response.success(usersRes);
default:
return Response.badRequest('Unknown object.');
}
} catch (e) {
return Response.error(e);
}
}
}

View File

@@ -129,7 +129,7 @@
"message": "Self-hosted Environment" "message": "Self-hosted Environment"
}, },
"selfHostedEnvironmentFooter": { "selfHostedEnvironmentFooter": {
"message": "Specify the base URL of your on-premise hosted Bitwarden installation." "message": "Specify the base URL of your on-premises hosted Bitwarden installation."
}, },
"customEnvironment": { "customEnvironment": {
"message": "Custom Environment" "message": "Custom Environment"
@@ -569,6 +569,13 @@
"hideToTray": { "hideToTray": {
"message": "Hide to Tray" "message": "Hide to Tray"
}, },
"alwaysOnTop": {
"message": "Always on Top",
"description": "Application window should always stay on top of other windows"
},
"hideToMenuBar": {
"message": "Hide to Menu Bar"
},
"savedSetting": { "savedSetting": {
"message": "Saved setting `$SETTING_NAME$`.", "message": "Saved setting `$SETTING_NAME$`.",
"placeholders": { "placeholders": {
@@ -577,5 +584,8 @@
"example": "server" "example": "server"
} }
} }
},
"overwriteExisting": {
"message": "Overwrite existing organization users based on current sync settings."
} }
} }

View File

@@ -52,7 +52,7 @@ export class Main {
this.i18nService = new I18nService('en', './locales/'); this.i18nService = new I18nService('en', './locales/');
this.storageService = new ElectronStorageService(app.getPath('userData')); this.storageService = new ElectronStorageService(app.getPath('userData'));
this.windowMain = new WindowMain(this.storageService, 800, 600); this.windowMain = new WindowMain(this.storageService, false, 800, 600);
this.menuMain = new MenuMain(this); this.menuMain = new MenuMain(this);
this.updaterMain = new UpdaterMain(this.i18nService, this.windowMain, 'directory-connector', () => { this.updaterMain = new UpdaterMain(this.i18nService, this.windowMain, 'directory-connector', () => {
this.messagingService.send('checkingForUpdate'); this.messagingService.send('checkingForUpdate');

View File

@@ -48,11 +48,19 @@ export class MenuMain extends BaseMenu {
template[template.length - 1].submenu = this.macWindowSubmenuOptions; template[template.length - 1].submenu = this.macWindowSubmenuOptions;
} }
(template[template.length - 1].submenu as MenuItemConstructorOptions[]).splice(1, 0, { (template[template.length - 1].submenu as MenuItemConstructorOptions[]).splice(1, 0,
label: this.main.i18nService.t('hideToTray'), {
click: () => this.main.messagingService.send('hideToTray'), label: this.main.i18nService.t(process.platform === 'darwin' ? 'hideToMenuBar' : 'hideToTray'),
accelerator: 'CmdOrCtrl+Shift+M', click: () => this.main.messagingService.send('hideToTray'),
}); accelerator: 'CmdOrCtrl+Shift+M',
},
{
type: 'checkbox',
label: this.main.i18nService.t('alwaysOnTop'),
checked: this.windowMain.win.isAlwaysOnTop(),
click: () => this.main.windowMain.toggleAlwaysOnTop(),
accelerator: 'CmdOrCtrl+Shift+T',
});
this.menu = Menu.buildFromTemplate(template); this.menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(this.menu); Menu.setApplicationMenu(this.menu);

View File

@@ -5,6 +5,7 @@ export class SyncConfiguration {
userFilter: string; userFilter: string;
groupFilter: string; groupFilter: string;
removeDisabled = false; removeDisabled = false;
overwriteExisting = false;
// Ldap properties // Ldap properties
groupObjectClass: string; groupObjectClass: string;
userObjectClass: string; userObjectClass: string;

View File

@@ -2,7 +2,7 @@
"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.5.0", "version": "2.6.2",
"author": "8bit Solutions LLC <hello@bitwarden.com> (https://bitwarden.com)", "author": "8bit Solutions LLC <hello@bitwarden.com> (https://bitwarden.com)",
"homepage": "https://bitwarden.com", "homepage": "https://bitwarden.com",
"license": "GPL-3.0", "license": "GPL-3.0",
@@ -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.1.2",
"keytar": "4.4.1" "keytar": "4.13.0"
} }
} }

View File

@@ -1,10 +1,12 @@
import * as chk from 'chalk'; import * as chk from 'chalk';
import * as program from 'commander'; import * as program from 'commander';
import * as path from 'path';
import { Main } from './bwdc'; import { Main } from './bwdc';
import { ClearCacheCommand } from './commands/clearCache.command'; import { ClearCacheCommand } from './commands/clearCache.command';
import { ConfigCommand } from './commands/config.command'; import { ConfigCommand } from './commands/config.command';
import { LastSyncCommand } from './commands/lastSync.command';
import { SyncCommand } from './commands/sync.command'; import { SyncCommand } from './commands/sync.command';
import { TestCommand } from './commands/test.command'; import { TestCommand } from './commands/test.command';
@@ -14,12 +16,16 @@ import { UpdateCommand } from 'jslib/cli/commands/update.command';
import { BaseProgram } from 'jslib/cli/baseProgram'; import { BaseProgram } from 'jslib/cli/baseProgram';
import { Response } from 'jslib/cli/models/response';
import { StringResponse } from 'jslib/cli/models/response/stringResponse';
const chalk = chk.default; const chalk = chk.default;
const writeLn = (s: string, finalLine: boolean = false) => { const writeLn = (s: string, finalLine: boolean = false, error: boolean = false) => {
const stream = error ? process.stderr : process.stdout;
if (finalLine && process.platform === 'win32') { if (finalLine && process.platform === 'win32') {
process.stdout.write(s); stream.write(s);
} else { } else {
process.stdout.write(s + '\n'); stream.write(s + '\n');
} }
}; };
@@ -53,16 +59,18 @@ export class Program extends BaseProgram {
}); });
program.on('command:*', () => { program.on('command:*', () => {
writeLn(chalk.redBright('Invalid command: ' + program.args.join(' '))); writeLn(chalk.redBright('Invalid command: ' + program.args.join(' ')), false, true);
writeLn('See --help for a list of available commands.', true); writeLn('See --help for a list of available commands.', true, true);
process.exitCode = 1; process.exitCode = 1;
}); });
program.on('--help', () => { program.on('--help', () => {
writeLn('\n Examples:'); writeLn('\n Examples:');
writeLn(''); writeLn('');
writeLn(' bwdc login');
writeLn(' bwdc test'); writeLn(' bwdc test');
writeLn(' bwdc sync'); writeLn(' bwdc sync');
writeLn(' bwdc last-sync');
writeLn(' bwdc config server https://bw.company.com'); writeLn(' bwdc config server https://bw.company.com');
writeLn(' bwdc update'); writeLn(' bwdc update');
writeLn('', true); writeLn('', true);
@@ -143,6 +151,27 @@ export class Program extends BaseProgram {
this.processResponse(response); this.processResponse(response);
}); });
program
.command('last-sync <object>')
.description('Get the last successful sync date.')
.on('--help', () => {
writeLn('\n Notes:');
writeLn('');
writeLn(' Returns empty response if no sync has been performed for the given object.');
writeLn('');
writeLn(' Examples:');
writeLn('');
writeLn(' bwdc last-sync groups');
writeLn(' bwdc last-sync users');
writeLn('', true);
})
.action(async (object: string, cmd: program.Command) => {
await this.exitIfNotAuthed();
const command = new LastSyncCommand(this.main.configurationService);
const response = await command.run(object, cmd);
this.processResponse(response);
});
program program
.command('config <setting> <value>') .command('config <setting> <value>')
.description('Configure settings.') .description('Configure settings.')
@@ -174,6 +203,20 @@ export class Program extends BaseProgram {
this.processResponse(response); this.processResponse(response);
}); });
program
.command('data-file')
.description('Path to data.json database file.')
.on('--help', () => {
writeLn('\n Examples:');
writeLn('');
writeLn(' bwdc data-file');
writeLn('', true);
})
.action(() => {
this.processResponse(
Response.success(new StringResponse(path.join(this.main.dataFilePath, 'data.json'))));
});
program program
.command('clear-cache') .command('clear-cache')
.description('Clear the sync cache.') .description('Clear the sync cache.')

View File

@@ -18,6 +18,7 @@ import { I18nService } from 'jslib/abstractions/i18n.service';
import { LogService } from 'jslib/abstractions/log.service'; import { LogService } from 'jslib/abstractions/log.service';
const NextLink = '@odata.nextLink'; const NextLink = '@odata.nextLink';
const DeltaLink = '@odata.deltaLink';
const ObjectType = '@odata.type'; const ObjectType = '@odata.type';
enum UserSetType { enum UserSetType {
@@ -59,7 +60,9 @@ export class AzureDirectoryService extends BaseDirectoryService implements Direc
let users: UserEntry[]; let users: UserEntry[];
if (this.syncConfig.users) { if (this.syncConfig.users) {
users = await this.getUsers(); users = await this.getCurrentUsers();
const deletedUsers = await this.getDeletedUsers(force, !test);
users = users.concat(deletedUsers);
} }
let groups: GroupEntry[]; let groups: GroupEntry[];
@@ -72,7 +75,7 @@ export class AzureDirectoryService extends BaseDirectoryService implements Direc
return [groups, users]; return [groups, users];
} }
private async getUsers(): 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');
@@ -111,6 +114,61 @@ export class AzureDirectoryService extends BaseDirectoryService implements Direc
return entries; return entries;
} }
private async getDeletedUsers(force: boolean, saveDelta: boolean): Promise<UserEntry[]> {
const entryIds = new Set<string>();
const entries: UserEntry[] = [];
let res: any = null;
const token = await this.configurationService.getUserDeltaToken();
if (!force && token != null) {
try {
const deltaReq = this.client.api(token);
res = await deltaReq.get();
} catch {
res = null;
}
}
if (res == null) {
const userReq = this.client.api('/users/delta');
res = await userReq.get();
}
const setFilter = this.createCustomUserSet(this.syncConfig.userFilter);
while (true) {
const users: graphType.User[] = res.value;
if (users != null) {
for (const user of users) {
if (user.id == null || entryIds.has(user.id)) {
continue;
}
const entry = this.buildUser(user);
if (!entry.disabled && !entry.deleted) {
continue;
}
if (await this.filterOutUserResult(setFilter, entry)) {
continue;
}
entries.push(entry);
entryIds.add(user.id);
}
}
if (res[NextLink] == null) {
if (res[DeltaLink] != null && saveDelta) {
await this.configurationService.saveUserDeltaToken(res[DeltaLink]);
}
break;
} else {
const nextReq = this.client.api(res[NextLink]);
res = await nextReq.get();
}
}
return entries;
}
private createCustomUserSet(filter: string): [UserSetType, Set<string>] { private createCustomUserSet(filter: string): [UserSetType, Set<string>] {
if (filter == null || filter === '') { if (filter == null || filter === '') {
return null; return null;

View File

@@ -25,7 +25,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,19 +34,21 @@ export class ConfigurationService {
return config; return config;
} }
switch (type) { if (this.useSecureStorageForSecrets) {
case DirectoryType.Ldap: switch (type) {
(config as any).password = await this.secureStorageService.get<string>(Keys.ldap); case DirectoryType.Ldap:
break; (config as any).password = await this.secureStorageService.get<string>(Keys.ldap);
case DirectoryType.AzureActiveDirectory: break;
(config as any).key = await this.secureStorageService.get<string>(Keys.azure); case DirectoryType.AzureActiveDirectory:
break; (config as any).key = await this.secureStorageService.get<string>(Keys.azure);
case DirectoryType.Okta: break;
(config as any).token = await this.secureStorageService.get<string>(Keys.okta); case DirectoryType.Okta:
break; (config as any).token = await this.secureStorageService.get<string>(Keys.okta);
case DirectoryType.GSuite: break;
(config as any).privateKey = await this.secureStorageService.get<string>(Keys.gsuite); case DirectoryType.GSuite:
break; (config as any).privateKey = await this.secureStorageService.get<string>(Keys.gsuite);
break;
}
} }
return config; return config;
} }
@@ -53,41 +56,43 @@ export class ConfigurationService {
async saveDirectory(type: DirectoryType, async saveDirectory(type: DirectoryType,
config: LdapConfiguration | GSuiteConfiguration | AzureConfiguration | OktaConfiguration): Promise<any> { config: LdapConfiguration | GSuiteConfiguration | AzureConfiguration | OktaConfiguration): Promise<any> {
const savedConfig: any = Object.assign({}, config); const savedConfig: any = Object.assign({}, config);
switch (type) { if (this.useSecureStorageForSecrets) {
case DirectoryType.Ldap: switch (type) {
if (savedConfig.password == null) { case DirectoryType.Ldap:
await this.secureStorageService.remove(Keys.ldap); if (savedConfig.password == null) {
} else { await this.secureStorageService.remove(Keys.ldap);
await this.secureStorageService.save(Keys.ldap, savedConfig.password); } else {
savedConfig.password = StoredSecurely; await this.secureStorageService.save(Keys.ldap, savedConfig.password);
} savedConfig.password = StoredSecurely;
break; }
case DirectoryType.AzureActiveDirectory: break;
if (savedConfig.key == null) { case DirectoryType.AzureActiveDirectory:
await this.secureStorageService.remove(Keys.azure); if (savedConfig.key == null) {
} else { await this.secureStorageService.remove(Keys.azure);
await this.secureStorageService.save(Keys.azure, savedConfig.key); } else {
savedConfig.key = StoredSecurely; await this.secureStorageService.save(Keys.azure, savedConfig.key);
} savedConfig.key = StoredSecurely;
break; }
case DirectoryType.Okta: break;
if (savedConfig.token == null) { case DirectoryType.Okta:
await this.secureStorageService.remove(Keys.okta); if (savedConfig.token == null) {
} else { await this.secureStorageService.remove(Keys.okta);
await this.secureStorageService.save(Keys.okta, savedConfig.token); } else {
savedConfig.token = StoredSecurely; await this.secureStorageService.save(Keys.okta, savedConfig.token);
} savedConfig.token = StoredSecurely;
break; }
case DirectoryType.GSuite: break;
if (savedConfig.privateKey == null) { case DirectoryType.GSuite:
await this.secureStorageService.remove(Keys.gsuite); if (savedConfig.privateKey == null) {
} else { await this.secureStorageService.remove(Keys.gsuite);
(config as GSuiteConfiguration).privateKey = savedConfig.privateKey = } else {
savedConfig.privateKey.replace(/\\n/g, '\n'); (config as GSuiteConfiguration).privateKey = savedConfig.privateKey =
await this.secureStorageService.save(Keys.gsuite, savedConfig.privateKey); savedConfig.privateKey.replace(/\\n/g, '\n');
savedConfig.privateKey = StoredSecurely; await this.secureStorageService.save(Keys.gsuite, savedConfig.privateKey);
} savedConfig.privateKey = StoredSecurely;
break; }
break;
}
} }
await this.storageService.save(Keys.directoryConfigPrefix + type, savedConfig); await this.storageService.save(Keys.directoryConfigPrefix + type, savedConfig);
} }

View File

@@ -67,45 +67,59 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements Dire
private async getUsers(): Promise<UserEntry[]> { private async getUsers(): Promise<UserEntry[]> {
const entries: UserEntry[] = []; const entries: UserEntry[] = [];
const query = this.createDirectoryQuery(this.syncConfig.userFilter); const query = this.createDirectoryQuery(this.syncConfig.userFilter);
let nextPageToken: string = null;
this.logService.info('Querying users.');
let p = Object.assign({ query: query }, this.authParams);
const res = await this.service.users.list(p);
if (res.status !== 200) {
throw new Error('User list API failed: ' + res.statusText);
}
const filter = this.createCustomSet(this.syncConfig.userFilter); const filter = this.createCustomSet(this.syncConfig.userFilter);
if (res.data.users != null) { while (true) {
for (const user of res.data.users) { this.logService.info('Querying users - nextPageToken:' + nextPageToken);
if (this.filterOutResult(filter, user.primaryEmail)) { const p = Object.assign({ query: query, pageToken: nextPageToken }, this.authParams);
continue; const res = await this.service.users.list(p);
} if (res.status !== 200) {
throw new Error('User list API failed: ' + res.statusText);
}
const entry = this.buildUser(user, false); nextPageToken = res.data.nextPageToken;
if (entry != null) { if (res.data.users != null) {
entries.push(entry); for (const user of res.data.users) {
if (this.filterOutResult(filter, user.primaryEmail)) {
continue;
}
const entry = this.buildUser(user, false);
if (entry != null) {
entries.push(entry);
}
} }
} }
if (nextPageToken == null) {
break;
}
} }
this.logService.info('Querying deleted users.'); nextPageToken = null;
p = Object.assign({ showDeleted: true, query: query }, this.authParams); while (true) {
const delRes = await this.service.users.list(p); this.logService.info('Querying deleted users - nextPageToken:' + nextPageToken);
if (delRes.status !== 200) { const p = Object.assign({ showDeleted: true, query: query, pageToken: nextPageToken }, this.authParams);
throw new Error('Deleted user list API failed: ' + delRes.statusText); const delRes = await this.service.users.list(p);
} if (delRes.status !== 200) {
throw new Error('Deleted user list API failed: ' + delRes.statusText);
}
if (delRes.data.users != null) { nextPageToken = delRes.data.nextPageToken;
for (const user of delRes.data.users) { if (delRes.data.users != null) {
if (this.filterOutResult(filter, user.primaryEmail)) { for (const user of delRes.data.users) {
continue; if (this.filterOutResult(filter, user.primaryEmail)) {
continue;
}
const entry = this.buildUser(user, true);
if (entry != null) {
entries.push(entry);
}
} }
}
const entry = this.buildUser(user, true); if (nextPageToken == null) {
if (entry != null) { break;
entries.push(entry);
}
} }
} }
@@ -128,56 +142,75 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements Dire
private async getGroups(setFilter: [boolean, Set<string>]): Promise<GroupEntry[]> { private async getGroups(setFilter: [boolean, Set<string>]): Promise<GroupEntry[]> {
const entries: GroupEntry[] = []; const entries: GroupEntry[] = [];
let nextPageToken: string = null;
this.logService.info('Querying groups.'); while (true) {
const res = await this.service.groups.list(this.authParams); this.logService.info('Querying groups - nextPageToken:' + nextPageToken);
if (res.status !== 200) { const p = Object.assign({ pageToken: nextPageToken }, this.authParams);
throw new Error('Group list API failed: ' + res.statusText); const res = await this.service.groups.list(p);
} if (res.status !== 200) {
if (res.data.groups != null) { throw new Error('Group list API failed: ' + res.statusText);
for (const group of res.data.groups) { }
if (!this.filterOutResult(setFilter, group.name)) {
const entry = await this.buildGroup(group); nextPageToken = res.data.nextPageToken;
entries.push(entry); if (res.data.groups != null) {
for (const group of res.data.groups) {
if (!this.filterOutResult(setFilter, group.name)) {
const entry = await this.buildGroup(group);
entries.push(entry);
}
} }
} }
if (nextPageToken == null) {
break;
}
} }
return entries; return entries;
} }
private async buildGroup(group: admin_directory_v1.Schema$Group) { private async buildGroup(group: admin_directory_v1.Schema$Group) {
let nextPageToken: string = null;
const entry = new GroupEntry(); const entry = new GroupEntry();
entry.referenceId = group.id; entry.referenceId = group.id;
entry.externalId = group.id; entry.externalId = group.id;
entry.name = group.name; entry.name = group.name;
const p = Object.assign({ groupKey: group.id }, this.authParams); while (true) {
const memRes = await this.service.members.list(p); const p = Object.assign({ groupKey: group.id, pageToken: nextPageToken }, this.authParams);
if (memRes.status !== 200) { const memRes = await this.service.members.list(p);
this.logService.warning('Group member list API failed: ' + memRes.statusText); if (memRes.status !== 200) {
return entry; this.logService.warning('Group member list API failed: ' + memRes.statusText);
} return entry;
}
if (memRes.data.members != null) { nextPageToken = memRes.data.nextPageToken;
for (const member of memRes.data.members) { if (memRes.data.members != null) {
if (member.type == null) { for (const member of memRes.data.members) {
continue; if (member.type == null) {
} continue;
if (member.role == null || member.role.toLowerCase() !== 'member') { }
continue; if (member.role == null || member.role.toLowerCase() !== 'member') {
} continue;
if (member.status == null || member.status.toLowerCase() !== 'active') { }
continue; if (member.status == null || member.status.toLowerCase() !== 'active') {
} continue;
}
const type = member.type.toLowerCase(); const type = member.type.toLowerCase();
if (type === 'user') { if (type === 'user') {
entry.userMemberExternalIds.add(member.id); entry.userMemberExternalIds.add(member.id);
} else if (type === 'group') { } else if (type === 'group') {
entry.groupMemberReferenceIds.add(member.id); entry.groupMemberReferenceIds.add(member.id);
}
} }
} }
if (nextPageToken == null) {
break;
}
} }
return entry; return entry;
@@ -191,7 +224,7 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements Dire
this.client = new google.auth.JWT({ this.client = new google.auth.JWT({
email: this.dirConfig.clientEmail, email: this.dirConfig.clientEmail,
key: this.dirConfig.privateKey, key: this.dirConfig.privateKey != null ? this.dirConfig.privateKey.trimLeft() : null,
subject: this.dirConfig.adminUser, subject: this.dirConfig.adminUser,
scopes: [ scopes: [
'https://www.googleapis.com/auth/admin.directory.user.readonly', 'https://www.googleapis.com/auth/admin.directory.user.readonly',
@@ -204,9 +237,11 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements Dire
this.authParams = { this.authParams = {
auth: this.client, auth: this.client,
domain: this.dirConfig.domain,
}; };
if (this.dirConfig.customer != null) { if (this.dirConfig.domain != null && this.dirConfig.domain.trim() !== '') {
this.authParams.domain = this.dirConfig.domain;
}
if (this.dirConfig.customer != null && this.dirConfig.customer.trim() !== '') {
this.authParams.customer = this.dirConfig.customer; this.authParams.customer = this.dirConfig.customer;
} }
} }

View File

@@ -48,7 +48,7 @@ export class SyncService {
this.messagingService.send('dirSyncStarted'); this.messagingService.send('dirSyncStarted');
try { try {
const entries = await directoryService.getEntries(force, test); const entries = await directoryService.getEntries(force || syncConfig.overwriteExisting, test);
let groups = entries[0]; let groups = entries[0];
let users = entries[1]; let users = entries[1];
@@ -65,7 +65,7 @@ export class SyncService {
return [groups, users]; return [groups, users];
} }
const req = this.buildRequest(groups, users, syncConfig.removeDisabled); const req = this.buildRequest(groups, users, syncConfig.removeDisabled, syncConfig.overwriteExisting);
const reqJson = JSON.stringify(req); const reqJson = JSON.stringify(req);
let hash: string = null; let hash: string = null;
@@ -128,8 +128,10 @@ export class SyncService {
} }
} }
private buildRequest(groups: GroupEntry[], users: UserEntry[], removeDisabled: boolean): ImportDirectoryRequest { private buildRequest(groups: GroupEntry[], users: UserEntry[], removeDisabled: boolean,
overwriteExisting: boolean): ImportDirectoryRequest {
const model = new ImportDirectoryRequest(); const model = new ImportDirectoryRequest();
model.overwriteExisting = overwriteExisting;
if (groups != null) { if (groups != null) {
for (const g of groups) { for (const g of groups) {

View File

@@ -49,10 +49,11 @@
"jslib/src/abstractions/passwordGeneration.service.ts", "jslib/src/abstractions/passwordGeneration.service.ts",
"jslib/src/angular/components/export.component.ts", "jslib/src/angular/components/export.component.ts",
"jslib/src/angular/components/register.component.ts", "jslib/src/angular/components/register.component.ts",
"jslib/src/angular/components/add-edit.component.ts",
"jslib/src/angular/components/password-generator.component.ts", "jslib/src/angular/components/password-generator.component.ts",
"jslib/src/angular/components/password-generator-history.component.ts", "jslib/src/angular/components/password-generator-history.component.ts",
"jslib/src/angular/pipes/color-password.pipe.ts", "jslib/src/angular/pipes/color-password.pipe.ts",
"jslib/src/angular/directives/flex-copy.directive.ts", "jslib/src/angular/directives/select-copy.directive.ts",
"jslib/src/importers", "jslib/src/importers",
"dist", "dist",
"dist-cli", "dist-cli",