From 096196fcd512944d1c3d9c007647a1319b032639 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Mon, 20 Dec 2021 17:14:18 +0100 Subject: [PATCH] Apply Prettier (#194) --- .github/PULL_REQUEST_TEMPLATE.md | 14 +- .vscode/launch.json | 66 +- README.md | 1 + SECURITY.md | 2 +- scripts/notarize.js | 30 +- scripts/sign.js | 29 +- src/app/accounts/apiKey.component.html | 97 +- src/app/accounts/apiKey.component.ts | 163 +-- src/app/accounts/environment.component.html | 100 +- src/app/accounts/environment.component.ts | 25 +- src/app/app-routing.module.ts | 99 +- src/app/app.component.ts | 287 ++--- src/app/app.module.ts | 128 +- src/app/main.ts | 12 +- src/app/services/auth-guard.service.ts | 34 +- src/app/services/launch-guard.service.ts | 27 +- src/app/services/services.module.ts | 401 +++---- src/app/tabs/dashboard.component.html | 199 ++-- src/app/tabs/dashboard.component.ts | 219 ++-- src/app/tabs/more.component.html | 62 +- src/app/tabs/more.component.ts | 126 +- src/app/tabs/settings.component.html | 1168 ++++++++++++------- src/app/tabs/settings.component.ts | 276 ++--- src/app/tabs/tabs.component.html | 42 +- src/app/tabs/tabs.component.ts | 8 +- src/bwdc.ts | 430 ++++--- src/commands/clearCache.command.ts | 31 +- src/commands/config.command.ts | 264 +++-- src/commands/lastSync.command.ts | 46 +- src/commands/sync.command.ts | 34 +- src/commands/test.command.ts | 34 +- src/enums/directoryType.ts | 10 +- src/global.d.ts | 2 +- src/index.html | 23 +- src/main.ts | 212 ++-- src/main/menu.main.ts | 119 +- src/main/messaging.main.ts | 107 +- src/models/azureConfiguration.ts | 8 +- src/models/entry.ts | 10 +- src/models/groupEntry.ts | 20 +- src/models/gsuiteConfiguration.ts | 10 +- src/models/ldapConfiguration.ts | 32 +- src/models/oktaConfiguration.ts | 4 +- src/models/oneLoginConfiguration.ts | 6 +- src/models/response/groupResponse.ts | 18 +- src/models/response/testResponse.ts | 35 +- src/models/response/userResponse.ts | 14 +- src/models/simResult.ts | 14 +- src/models/syncConfiguration.ts | 42 +- src/models/userEntry.ts | 20 +- src/program.ts | 576 ++++----- src/scss/bootstrap.scss | 30 +- src/scss/environment.scss | 6 +- src/scss/misc.scss | 177 ++- src/scss/plugins.scss | 199 ++-- src/services/api.service.ts | 49 +- src/services/auth.service.ts | 138 ++- src/services/azure-directory.service.ts | 885 +++++++------- src/services/baseDirectory.service.ts | 158 +-- src/services/configuration.service.ts | 433 +++---- src/services/directory.service.ts | 6 +- src/services/gsuite-directory.service.ts | 458 ++++---- src/services/i18n.service.ts | 25 +- src/services/keytarSecureStorage.service.ts | 38 +- src/services/ldap-directory.service.ts | 874 +++++++------- src/services/lowdbStorage.service.ts | 44 +- src/services/nodeApi.service.ts | 37 +- src/services/okta-directory.service.ts | 474 ++++---- src/services/onelogin-directory.service.ts | 346 +++--- src/services/sync.service.ts | 450 +++---- src/utils.ts | 179 +-- tsconfig.json | 25 +- tslint.json | 21 +- webpack.cli.js | 128 +- webpack.main.js | 118 +- webpack.renderer.js | 230 ++-- 76 files changed, 6056 insertions(+), 5208 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 8c8bf607..2f8a5a9e 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,4 +1,5 @@ ## Type of change + - [ ] Bug fix - [ ] New feature development - [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc) @@ -6,27 +7,26 @@ - [ ] Other ## Objective + - - ## Code changes + -* **file.ext:** Description of what was changed and why +- **file.ext:** Description of what was changed and why ## Screenshots + - - ## Testing requirements + - - ## Before you submit + - [ ] I have checked for **linting** errors (`npm run lint`) (required) - [ ] I have added **unit tests** where it makes sense to do so (encouraged but not required) - [ ] This change requires a **documentation update** (notify the documentation team) diff --git a/.vscode/launch.json b/.vscode/launch.json index b39fd170..4985b2bc 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,48 +1,40 @@ { "version": "0.2.0", "configurations": [ - { - "type": "node", - "request": "launch", - "name": "Electron: Main", - "protocol": "inspector", - "cwd": "${workspaceRoot}/build", - "runtimeArgs": [ - "--remote-debugging-port=9223", - "." - ], - "windows": { - "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd" - }, - "sourceMaps": true + { + "type": "node", + "request": "launch", + "name": "Electron: Main", + "protocol": "inspector", + "cwd": "${workspaceRoot}/build", + "runtimeArgs": ["--remote-debugging-port=9223", "."], + "windows": { + "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd" }, - { - "name": "Electron: Renderer", - "type": "chrome", - "request": "attach", - "port": 9223, - "webRoot": "${workspaceFolder}/build", - "sourceMaps": true + "sourceMaps": true }, { - "type": "node", - "request": "launch", - "name": "Debug CLI", - "protocol": "inspector", - "cwd": "${workspaceFolder}", - "program": "${workspaceFolder}/build-cli/bwdc.js", - "args": [ - "sync" - ] + "name": "Electron: Renderer", + "type": "chrome", + "request": "attach", + "port": 9223, + "webRoot": "${workspaceFolder}/build", + "sourceMaps": true + }, + { + "type": "node", + "request": "launch", + "name": "Debug CLI", + "protocol": "inspector", + "cwd": "${workspaceFolder}", + "program": "${workspaceFolder}/build-cli/bwdc.js", + "args": ["sync"] } ], "compounds": [ - { - "name": "Electron: All", - "configurations": [ - "Electron: Main", - "Electron: Renderer" - ] - } + { + "name": "Electron: All", + "configurations": ["Electron: Main", "Electron: Renderer"] + } ] } diff --git a/README.md b/README.md index d90f6f7e..e1c2ab46 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ The Bitwarden Directory Connector is a a desktop application used to sync your Bitwarden enterprise organization to an existing directory of users and groups. Supported directories: + - Active Directory - Any other LDAP-based directory - Azure Active Directory diff --git a/SECURITY.md b/SECURITY.md index ef94f0b4..7a055501 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -7,7 +7,7 @@ notify us. We welcome working with you to resolve the issue promptly. Thanks in - Let us know as soon as possible upon discovery of a potential security issue, and we'll make every effort to quickly resolve the issue. - Provide us a reasonable amount of time to resolve the issue before any disclosure to the public or a - third-party. We may publicly disclose the issue before resolving it, if appropriate. + third-party. We may publicly disclose the issue before resolving it, if appropriate. - Make a good faith effort to avoid privacy violations, destruction of data, and interruption or degradation of our service. Only interact with accounts you own or with explicit permission of the account holder. diff --git a/scripts/notarize.js b/scripts/notarize.js index 1383ae99..8aa15912 100644 --- a/scripts/notarize.js +++ b/scripts/notarize.js @@ -1,18 +1,18 @@ -require('dotenv').config(); -const { notarize } = require('electron-notarize'); +require("dotenv").config(); +const { notarize } = require("electron-notarize"); exports.default = async function notarizing(context) { - const { electronPlatformName, appOutDir } = context; - if (electronPlatformName !== 'darwin') { - return; - } - const appleId = process.env.APPLE_ID_USERNAME || process.env.APPLEID; - const appleIdPassword = process.env.APPLE_ID_PASSWORD || `@keychain:AC_PASSWORD`; - const appName = context.packager.appInfo.productFilename; - return await notarize({ - appBundleId: 'com.bitwarden.directory-connector', - appPath: `${appOutDir}/${appName}.app`, - appleId: appleId, - appleIdPassword: appleIdPassword, - }); + const { electronPlatformName, appOutDir } = context; + if (electronPlatformName !== "darwin") { + return; + } + const appleId = process.env.APPLE_ID_USERNAME || process.env.APPLEID; + const appleIdPassword = process.env.APPLE_ID_PASSWORD || `@keychain:AC_PASSWORD`; + const appName = context.packager.appInfo.productFilename; + return await notarize({ + appBundleId: "com.bitwarden.directory-connector", + appPath: `${appOutDir}/${appName}.app`, + appleId: appleId, + appleIdPassword: appleIdPassword, + }); }; diff --git a/scripts/sign.js b/scripts/sign.js index 1cf95813..2051cb70 100644 --- a/scripts/sign.js +++ b/scripts/sign.js @@ -1,22 +1,19 @@ -exports.default = async function(configuration) { - if ( - parseInt(process.env.ELECTRON_BUILDER_SIGN) === 1 && - configuration.path.slice(-4) == ".exe" - ) { - console.log(`[*] Signing file: ${configuration.path}`) +exports.default = async function (configuration) { + if (parseInt(process.env.ELECTRON_BUILDER_SIGN) === 1 && configuration.path.slice(-4) == ".exe") { + console.log(`[*] Signing file: ${configuration.path}`); require("child_process").execSync( `azuresigntool sign ` + - `-kvu ${process.env.SIGNING_VAULT_URL} ` + - `-kvi ${process.env.SIGNING_CLIENT_ID} ` + - `-kvt ${process.env.SIGNING_TENANT_ID} ` + - `-kvs ${process.env.SIGNING_CLIENT_SECRET} ` + - `-kvc ${process.env.SIGNING_CERT_NAME} ` + - `-fd ${configuration.hash} ` + - `-du ${configuration.site} ` + - `-tr http://timestamp.digicert.com ` + - `"${configuration.path}"`, + `-kvu ${process.env.SIGNING_VAULT_URL} ` + + `-kvi ${process.env.SIGNING_CLIENT_ID} ` + + `-kvt ${process.env.SIGNING_TENANT_ID} ` + + `-kvs ${process.env.SIGNING_CLIENT_SECRET} ` + + `-kvc ${process.env.SIGNING_CERT_NAME} ` + + `-fd ${configuration.hash} ` + + `-du ${configuration.site} ` + + `-tr http://timestamp.digicert.com ` + + `"${configuration.path}"`, { - stdio: "inherit" + stdio: "inherit", } ); } diff --git a/src/app/accounts/apiKey.component.html b/src/app/accounts/apiKey.component.html index ad4272d8..07c067ce 100644 --- a/src/app/accounts/apiKey.component.html +++ b/src/app/accounts/apiKey.component.html @@ -1,47 +1,60 @@
-
-
-
-

{{'welcome' | i18n}}

-

{{'logInDesc' | i18n}}

-
-
{{'logIn' | i18n}}
-
-
- - -
-
-
- -
- -
- -
-
-
-
-
-
- -
- -
-
-
+ +
+
+

{{ "welcome" | i18n }}

+

{{ "logInDesc" | i18n }}

+
+
{{ "logIn" | i18n }}
+
+
+ +
+
+
+ +
+ +
+ +
+
+
+
+
+
+ +
+ +
+
- +
+
+
diff --git a/src/app/accounts/apiKey.component.ts b/src/app/accounts/apiKey.component.ts index f9ffc683..cfc02e92 100644 --- a/src/app/accounts/apiKey.component.ts +++ b/src/app/accounts/apiKey.component.ts @@ -1,87 +1,110 @@ import { - Component, - ComponentFactoryResolver, - Input, - ViewChild, - ViewContainerRef, -} from '@angular/core'; -import { Router } from '@angular/router'; + Component, + ComponentFactoryResolver, + Input, + ViewChild, + ViewContainerRef, +} from "@angular/core"; +import { Router } from "@angular/router"; -import { EnvironmentComponent } from './environment.component'; +import { EnvironmentComponent } from "./environment.component"; -import { ApiKeyService } from 'jslib-common/abstractions/apiKey.service'; -import { AuthService } from 'jslib-common/abstractions/auth.service'; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { LogService } from 'jslib-common/abstractions/log.service'; -import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; +import { ApiKeyService } from "jslib-common/abstractions/apiKey.service"; +import { AuthService } from "jslib-common/abstractions/auth.service"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { LogService } from "jslib-common/abstractions/log.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; -import { ModalService } from 'jslib-angular/services/modal.service'; +import { ModalService } from "jslib-angular/services/modal.service"; -import { Utils } from 'jslib-common/misc/utils'; -import { ConfigurationService } from '../../services/configuration.service'; +import { Utils } from "jslib-common/misc/utils"; +import { ConfigurationService } from "../../services/configuration.service"; @Component({ - selector: 'app-apiKey', - templateUrl: 'apiKey.component.html', + selector: "app-apiKey", + templateUrl: "apiKey.component.html", }) export class ApiKeyComponent { - @ViewChild('environment', { read: ViewContainerRef, static: true }) environmentModal: ViewContainerRef; - @Input() clientId: string = ''; - @Input() clientSecret: string = ''; + @ViewChild("environment", { read: ViewContainerRef, static: true }) + environmentModal: ViewContainerRef; + @Input() clientId: string = ""; + @Input() clientSecret: string = ""; - formPromise: Promise; - successRoute = '/tabs/dashboard'; - showSecret: boolean = false; + formPromise: Promise; + successRoute = "/tabs/dashboard"; + showSecret: boolean = false; - constructor(private authService: AuthService, private apiKeyService: ApiKeyService, private router: Router, - private i18nService: I18nService, private componentFactoryResolver: ComponentFactoryResolver, - private configurationService: ConfigurationService, private platformUtilsService: PlatformUtilsService, - private modalService: ModalService, private logService: LogService) { } + constructor( + private authService: AuthService, + private apiKeyService: ApiKeyService, + private router: Router, + private i18nService: I18nService, + private componentFactoryResolver: ComponentFactoryResolver, + private configurationService: ConfigurationService, + private platformUtilsService: PlatformUtilsService, + private modalService: ModalService, + private logService: LogService + ) {} - async submit() { - if (this.clientId == null || this.clientId === '') { - this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), - this.i18nService.t('clientIdRequired')); - return; - } - if (!this.clientId.startsWith('organization')) { - this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), - this.i18nService.t('orgApiKeyRequired')); - return; - } - if (this.clientSecret == null || this.clientSecret === '') { - this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), - this.i18nService.t('clientSecretRequired')); - return; - } - const idParts = this.clientId.split('.'); + async submit() { + if (this.clientId == null || this.clientId === "") { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + this.i18nService.t("clientIdRequired") + ); + return; + } + if (!this.clientId.startsWith("organization")) { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + this.i18nService.t("orgApiKeyRequired") + ); + return; + } + if (this.clientSecret == null || this.clientSecret === "") { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + this.i18nService.t("clientSecretRequired") + ); + return; + } + const idParts = this.clientId.split("."); - if (idParts.length !== 2 || idParts[0] !== 'organization' || !Utils.isGuid(idParts[1])) { - this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), - this.i18nService.t('invalidClientId')); - return; - } - - try { - this.formPromise = this.authService.logInApiKey(this.clientId, this.clientSecret); - await this.formPromise; - const organizationId = await this.apiKeyService.getEntityId(); - await this.configurationService.saveOrganizationId(organizationId); - this.router.navigate([this.successRoute]); - } catch (e) { - this.logService.error(e); - } + if (idParts.length !== 2 || idParts[0] !== "organization" || !Utils.isGuid(idParts[1])) { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + this.i18nService.t("invalidClientId") + ); + return; } - async settings() { - const [modalRef, childComponent] = await this.modalService.openViewRef(EnvironmentComponent, this.environmentModal); + try { + this.formPromise = this.authService.logInApiKey(this.clientId, this.clientSecret); + await this.formPromise; + const organizationId = await this.apiKeyService.getEntityId(); + await this.configurationService.saveOrganizationId(organizationId); + this.router.navigate([this.successRoute]); + } catch (e) { + this.logService.error(e); + } + } - childComponent.onSaved.subscribe(() => { - modalRef.close(); - }); - } - toggleSecret() { - this.showSecret = !this.showSecret; - document.getElementById('client_secret').focus(); - } + async settings() { + const [modalRef, childComponent] = await this.modalService.openViewRef( + EnvironmentComponent, + this.environmentModal + ); + + childComponent.onSaved.subscribe(() => { + modalRef.close(); + }); + } + toggleSecret() { + this.showSecret = !this.showSecret; + document.getElementById("client_secret").focus(); + } } diff --git a/src/app/accounts/environment.component.html b/src/app/accounts/environment.component.html index 8f541ae3..88d55646 100644 --- a/src/app/accounts/environment.component.html +++ b/src/app/accounts/environment.component.html @@ -1,43 +1,61 @@ diff --git a/src/app/accounts/environment.component.ts b/src/app/accounts/environment.component.ts index ba8f34ff..1f4aeb38 100644 --- a/src/app/accounts/environment.component.ts +++ b/src/app/accounts/environment.component.ts @@ -1,18 +1,21 @@ -import { Component } from '@angular/core'; +import { Component } from "@angular/core"; -import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; +import { EnvironmentService } from "jslib-common/abstractions/environment.service"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; -import { EnvironmentComponent as BaseEnvironmentComponent } from 'jslib-angular/components/environment.component'; +import { EnvironmentComponent as BaseEnvironmentComponent } from "jslib-angular/components/environment.component"; @Component({ - selector: 'app-environment', - templateUrl: 'environment.component.html', + selector: "app-environment", + templateUrl: "environment.component.html", }) export class EnvironmentComponent extends BaseEnvironmentComponent { - constructor(environmentService: EnvironmentService, i18nService: I18nService, - platformUtilsService: PlatformUtilsService) { - super(platformUtilsService, environmentService, i18nService); - } + constructor( + environmentService: EnvironmentService, + i18nService: I18nService, + platformUtilsService: PlatformUtilsService + ) { + super(platformUtilsService, environmentService, i18nService); + } } diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 54550c20..8b9a778c 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -1,58 +1,57 @@ -import { NgModule } from '@angular/core'; -import { - RouterModule, - Routes, -} from '@angular/router'; +import { NgModule } from "@angular/core"; +import { RouterModule, Routes } from "@angular/router"; -import { AuthGuardService } from './services/auth-guard.service'; -import { LaunchGuardService } from './services/launch-guard.service'; +import { AuthGuardService } from "./services/auth-guard.service"; +import { LaunchGuardService } from "./services/launch-guard.service"; -import { ApiKeyComponent } from './accounts/apiKey.component'; -import { DashboardComponent } from './tabs/dashboard.component'; -import { MoreComponent } from './tabs/more.component'; -import { SettingsComponent } from './tabs/settings.component'; -import { TabsComponent } from './tabs/tabs.component'; +import { ApiKeyComponent } from "./accounts/apiKey.component"; +import { DashboardComponent } from "./tabs/dashboard.component"; +import { MoreComponent } from "./tabs/more.component"; +import { SettingsComponent } from "./tabs/settings.component"; +import { TabsComponent } from "./tabs/tabs.component"; const routes: Routes = [ - { path: '', redirectTo: '/login', pathMatch: 'full' }, - { - path: 'login', - component: ApiKeyComponent, - canActivate: [LaunchGuardService], - }, - { - path: 'tabs', - component: TabsComponent, - children: [ - { - path: '', - redirectTo: '/tabs/dashboard', - pathMatch: 'full', - }, - { - path: 'dashboard', - component: DashboardComponent, - canActivate: [AuthGuardService], - }, - { - path: 'settings', - component: SettingsComponent, - canActivate: [AuthGuardService], - }, - { - path: 'more', - component: MoreComponent, - canActivate: [AuthGuardService], - }, - ], - }, + { path: "", redirectTo: "/login", pathMatch: "full" }, + { + path: "login", + component: ApiKeyComponent, + canActivate: [LaunchGuardService], + }, + { + path: "tabs", + component: TabsComponent, + children: [ + { + path: "", + redirectTo: "/tabs/dashboard", + pathMatch: "full", + }, + { + path: "dashboard", + component: DashboardComponent, + canActivate: [AuthGuardService], + }, + { + path: "settings", + component: SettingsComponent, + canActivate: [AuthGuardService], + }, + { + path: "more", + component: MoreComponent, + canActivate: [AuthGuardService], + }, + ], + }, ]; @NgModule({ - imports: [RouterModule.forRoot(routes, { - useHash: true, - /*enableTracing: true,*/ - })], - exports: [RouterModule], + imports: [ + RouterModule.forRoot(routes, { + useHash: true, + /*enableTracing: true,*/ + }), + ], + exports: [RouterModule], }) -export class AppRoutingModule { } +export class AppRoutingModule {} diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 1786ac58..79136942 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,155 +1,166 @@ import { - Component, - NgZone, - OnInit, - SecurityContext, - ViewChild, - ViewContainerRef, -} from '@angular/core'; -import { DomSanitizer } from '@angular/platform-browser'; -import { Router } from '@angular/router'; -import { - IndividualConfig, - ToastrService, -} from 'ngx-toastr'; + Component, + NgZone, + OnInit, + SecurityContext, + ViewChild, + ViewContainerRef, +} from "@angular/core"; +import { DomSanitizer } from "@angular/platform-browser"; +import { Router } from "@angular/router"; +import { IndividualConfig, ToastrService } from "ngx-toastr"; -import { AuthService } from 'jslib-common/abstractions/auth.service'; -import { BroadcasterService } from 'jslib-common/abstractions/broadcaster.service'; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { LogService } from 'jslib-common/abstractions/log.service'; -import { MessagingService } from 'jslib-common/abstractions/messaging.service'; -import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; -import { StateService } from 'jslib-common/abstractions/state.service'; -import { TokenService } from 'jslib-common/abstractions/token.service'; -import { UserService } from 'jslib-common/abstractions/user.service'; +import { AuthService } from "jslib-common/abstractions/auth.service"; +import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { LogService } from "jslib-common/abstractions/log.service"; +import { MessagingService } from "jslib-common/abstractions/messaging.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; +import { StateService } from "jslib-common/abstractions/state.service"; +import { TokenService } from "jslib-common/abstractions/token.service"; +import { UserService } from "jslib-common/abstractions/user.service"; -import { ConfigurationService } from '../services/configuration.service'; -import { SyncService } from '../services/sync.service'; +import { ConfigurationService } from "../services/configuration.service"; +import { SyncService } from "../services/sync.service"; -const BroadcasterSubscriptionId = 'AppComponent'; +const BroadcasterSubscriptionId = "AppComponent"; @Component({ - selector: 'app-root', - styles: [], - template: ` - - `, + selector: "app-root", + styles: [], + template: ` + `, }) export class AppComponent implements OnInit { - @ViewChild('settings', { read: ViewContainerRef, static: true }) settingsRef: ViewContainerRef; + @ViewChild("settings", { read: ViewContainerRef, static: true }) settingsRef: ViewContainerRef; - constructor(private broadcasterService: BroadcasterService, private userService: UserService, - private tokenService: TokenService, - private authService: AuthService, private router: Router, - private toastrService: ToastrService, private i18nService: I18nService, - private sanitizer: DomSanitizer, private ngZone: NgZone, - private platformUtilsService: PlatformUtilsService, private messagingService: MessagingService, - private configurationService: ConfigurationService, private syncService: SyncService, - private stateService: StateService, private logService: LogService) { - } + constructor( + private broadcasterService: BroadcasterService, + private userService: UserService, + private tokenService: TokenService, + private authService: AuthService, + private router: Router, + private toastrService: ToastrService, + private i18nService: I18nService, + private sanitizer: DomSanitizer, + private ngZone: NgZone, + private platformUtilsService: PlatformUtilsService, + private messagingService: MessagingService, + private configurationService: ConfigurationService, + private syncService: SyncService, + private stateService: StateService, + private logService: LogService + ) {} - ngOnInit() { - this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => { - this.ngZone.run(async () => { - switch (message.command) { - case 'syncScheduleStarted': - case 'syncScheduleStopped': - this.stateService.save('syncingDir', message.command === 'syncScheduleStarted'); - break; - case 'logout': - this.logOut(!!message.expired); - break; - case 'checkDirSync': - try { - const syncConfig = await this.configurationService.getSync(); - if (syncConfig.interval == null || syncConfig.interval < 5) { - return; - } + ngOnInit() { + this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => { + this.ngZone.run(async () => { + switch (message.command) { + case "syncScheduleStarted": + case "syncScheduleStopped": + this.stateService.save("syncingDir", message.command === "syncScheduleStarted"); + break; + case "logout": + this.logOut(!!message.expired); + break; + case "checkDirSync": + try { + const syncConfig = await this.configurationService.getSync(); + if (syncConfig.interval == null || syncConfig.interval < 5) { + return; + } - const syncInterval = syncConfig.interval * 60000; - const lastGroupSync = await this.configurationService.getLastGroupSyncDate(); - const lastUserSync = await this.configurationService.getLastUserSyncDate(); - let lastSync: Date = null; - if (lastGroupSync != null && lastUserSync == null) { - lastSync = lastGroupSync; - } else if (lastGroupSync == null && lastUserSync != null) { - lastSync = lastUserSync; - } else if (lastGroupSync != null && lastUserSync != null) { - if (lastGroupSync.getTime() < lastUserSync.getTime()) { - lastSync = lastGroupSync; - } else { - lastSync = lastUserSync; - } - } - - let lastSyncAgo = syncInterval + 1; - if (lastSync != null) { - lastSyncAgo = new Date().getTime() - lastSync.getTime(); - } - - if (lastSyncAgo >= syncInterval) { - await this.syncService.sync(false, false); - } - } catch (e) { - this.logService.error(e); - } - - this.messagingService.send('scheduleNextDirSync'); - break; - case 'showToast': - this.showToast(message); - break; - case 'ssoCallback': - this.router.navigate(['sso'], { queryParams: { code: message.code, state: message.state } }); - break; - default: + const syncInterval = syncConfig.interval * 60000; + const lastGroupSync = await this.configurationService.getLastGroupSyncDate(); + const lastUserSync = await this.configurationService.getLastUserSyncDate(); + let lastSync: Date = null; + if (lastGroupSync != null && lastUserSync == null) { + lastSync = lastGroupSync; + } else if (lastGroupSync == null && lastUserSync != null) { + lastSync = lastUserSync; + } else if (lastGroupSync != null && lastUserSync != null) { + if (lastGroupSync.getTime() < lastUserSync.getTime()) { + lastSync = lastGroupSync; + } else { + lastSync = lastUserSync; } + } + + let lastSyncAgo = syncInterval + 1; + if (lastSync != null) { + lastSyncAgo = new Date().getTime() - lastSync.getTime(); + } + + if (lastSyncAgo >= syncInterval) { + await this.syncService.sync(false, false); + } + } catch (e) { + this.logService.error(e); + } + + this.messagingService.send("scheduleNextDirSync"); + break; + case "showToast": + this.showToast(message); + break; + case "ssoCallback": + this.router.navigate(["sso"], { + queryParams: { code: message.code, state: message.state }, }); - }); - } - - ngOnDestroy() { - this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); - } - - private async logOut(expired: boolean) { - const userId = await this.userService.getUserId(); - - await this.tokenService.clearToken(); - await this.userService.clear(); - - this.authService.logOut(async () => { - if (expired) { - this.platformUtilsService.showToast('warning', this.i18nService.t('loggedOut'), - this.i18nService.t('loginExpired')); - } - this.router.navigate(['login']); - }); - } - - private showToast(msg: any) { - let message = ''; - - const options: Partial = {}; - - if (typeof (msg.text) === 'string') { - message = msg.text; - } else if (msg.text.length === 1) { - message = msg.text[0]; - } else { - msg.text.forEach((t: string) => - message += ('

' + this.sanitizer.sanitize(SecurityContext.HTML, t) + '

')); - options.enableHtml = true; - } - if (msg.options != null) { - if (msg.options.trustedHtml === true) { - options.enableHtml = true; - } - if (msg.options.timeout != null && msg.options.timeout > 0) { - options.timeOut = msg.options.timeout; - } + break; + default: } + }); + }); + } - this.toastrService.show(message, msg.title, options, 'toast-' + msg.type); + ngOnDestroy() { + this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); + } + + private async logOut(expired: boolean) { + const userId = await this.userService.getUserId(); + + await this.tokenService.clearToken(); + await this.userService.clear(); + + this.authService.logOut(async () => { + if (expired) { + this.platformUtilsService.showToast( + "warning", + this.i18nService.t("loggedOut"), + this.i18nService.t("loginExpired") + ); + } + this.router.navigate(["login"]); + }); + } + + private showToast(msg: any) { + let message = ""; + + const options: Partial = {}; + + if (typeof msg.text === "string") { + message = msg.text; + } else if (msg.text.length === 1) { + message = msg.text[0]; + } else { + msg.text.forEach( + (t: string) => + (message += "

" + this.sanitizer.sanitize(SecurityContext.HTML, t) + "

") + ); + options.enableHtml = true; } + if (msg.options != null) { + if (msg.options.trustedHtml === true) { + options.enableHtml = true; + } + if (msg.options.timeout != null && msg.options.timeout > 0) { + options.timeOut = msg.options.timeout; + } + } + + this.toastrService.show(message, msg.title, options, "toast-" + msg.type); + } } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 84d55dfe..f20edf8e 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,74 +1,74 @@ -import 'core-js/stable'; -import 'zone.js/dist/zone'; +import "core-js/stable"; +import "zone.js/dist/zone"; -import { AppRoutingModule } from './app-routing.module'; -import { ServicesModule } from './services/services.module'; +import { AppRoutingModule } from "./app-routing.module"; +import { ServicesModule } from "./services/services.module"; -import { NgModule } from '@angular/core'; -import { FormsModule } from '@angular/forms'; -import { BrowserModule } from '@angular/platform-browser'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { NgModule } from "@angular/core"; +import { FormsModule } from "@angular/forms"; +import { BrowserModule } from "@angular/platform-browser"; +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 { BitwardenToastModule } from 'jslib-angular/components/toastr.component'; +import { CalloutComponent } from "jslib-angular/components/callout.component"; +import { IconComponent } from "jslib-angular/components/icon.component"; +import { BitwardenToastModule } from "jslib-angular/components/toastr.component"; -import { ApiKeyComponent } from './accounts/apiKey.component'; -import { EnvironmentComponent } from './accounts/environment.component'; -import { DashboardComponent } from './tabs/dashboard.component'; -import { MoreComponent } from './tabs/more.component'; -import { SettingsComponent } from './tabs/settings.component'; -import { TabsComponent } from './tabs/tabs.component'; +import { ApiKeyComponent } from "./accounts/apiKey.component"; +import { EnvironmentComponent } from "./accounts/environment.component"; +import { DashboardComponent } from "./tabs/dashboard.component"; +import { MoreComponent } from "./tabs/more.component"; +import { SettingsComponent } from "./tabs/settings.component"; +import { TabsComponent } from "./tabs/tabs.component"; -import { A11yTitleDirective } from 'jslib-angular/directives/a11y-title.directive'; -import { ApiActionDirective } from 'jslib-angular/directives/api-action.directive'; -import { AutofocusDirective } from 'jslib-angular/directives/autofocus.directive'; -import { BlurClickDirective } from 'jslib-angular/directives/blur-click.directive'; -import { BoxRowDirective } from 'jslib-angular/directives/box-row.directive'; -import { FallbackSrcDirective } from 'jslib-angular/directives/fallback-src.directive'; -import { StopClickDirective } from 'jslib-angular/directives/stop-click.directive'; -import { StopPropDirective } from 'jslib-angular/directives/stop-prop.directive'; +import { A11yTitleDirective } from "jslib-angular/directives/a11y-title.directive"; +import { ApiActionDirective } from "jslib-angular/directives/api-action.directive"; +import { AutofocusDirective } from "jslib-angular/directives/autofocus.directive"; +import { BlurClickDirective } from "jslib-angular/directives/blur-click.directive"; +import { BoxRowDirective } from "jslib-angular/directives/box-row.directive"; +import { FallbackSrcDirective } from "jslib-angular/directives/fallback-src.directive"; +import { StopClickDirective } from "jslib-angular/directives/stop-click.directive"; +import { StopPropDirective } from "jslib-angular/directives/stop-prop.directive"; -import { I18nPipe } from 'jslib-angular/pipes/i18n.pipe'; -import { SearchCiphersPipe } from 'jslib-angular/pipes/search-ciphers.pipe'; +import { I18nPipe } from "jslib-angular/pipes/i18n.pipe"; +import { SearchCiphersPipe } from "jslib-angular/pipes/search-ciphers.pipe"; @NgModule({ - imports: [ - BrowserModule, - BrowserAnimationsModule, - FormsModule, - AppRoutingModule, - ServicesModule, - BitwardenToastModule.forRoot({ - maxOpened: 5, - autoDismiss: true, - closeButton: true, - }), - ], - declarations: [ - A11yTitleDirective, - ApiActionDirective, - ApiKeyComponent, - AppComponent, - AutofocusDirective, - BlurClickDirective, - BoxRowDirective, - CalloutComponent, - DashboardComponent, - EnvironmentComponent, - FallbackSrcDirective, - I18nPipe, - IconComponent, - MoreComponent, - SearchCiphersPipe, - SettingsComponent, - StopClickDirective, - StopPropDirective, - TabsComponent, - ], - providers: [], - bootstrap: [AppComponent], + imports: [ + BrowserModule, + BrowserAnimationsModule, + FormsModule, + AppRoutingModule, + ServicesModule, + BitwardenToastModule.forRoot({ + maxOpened: 5, + autoDismiss: true, + closeButton: true, + }), + ], + declarations: [ + A11yTitleDirective, + ApiActionDirective, + ApiKeyComponent, + AppComponent, + AutofocusDirective, + BlurClickDirective, + BoxRowDirective, + CalloutComponent, + DashboardComponent, + EnvironmentComponent, + FallbackSrcDirective, + I18nPipe, + IconComponent, + MoreComponent, + SearchCiphersPipe, + SettingsComponent, + StopClickDirective, + StopPropDirective, + TabsComponent, + ], + providers: [], + bootstrap: [AppComponent], }) -export class AppModule { } +export class AppModule {} diff --git a/src/app/main.ts b/src/app/main.ts index a30eefb6..d3075155 100644 --- a/src/app/main.ts +++ b/src/app/main.ts @@ -1,15 +1,15 @@ -import { enableProdMode } from '@angular/core'; -import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; +import { enableProdMode } from "@angular/core"; +import { platformBrowserDynamic } from "@angular/platform-browser-dynamic"; -import { isDev } from 'jslib-electron/utils'; +import { isDev } from "jslib-electron/utils"; // tslint:disable-next-line -require('../scss/styles.scss'); +require("../scss/styles.scss"); -import { AppModule } from './app.module'; +import { AppModule } from "./app.module"; if (!isDev()) { - enableProdMode(); + enableProdMode(); } platformBrowserDynamic().bootstrapModule(AppModule, { preserveWhitespaces: true }); diff --git a/src/app/services/auth-guard.service.ts b/src/app/services/auth-guard.service.ts index 658fade9..c184a3c8 100644 --- a/src/app/services/auth-guard.service.ts +++ b/src/app/services/auth-guard.service.ts @@ -1,24 +1,24 @@ -import { Injectable } from '@angular/core'; -import { - CanActivate, - Router, -} from '@angular/router'; -import { ApiKeyService } from 'jslib-common/abstractions/apiKey.service'; +import { Injectable } from "@angular/core"; +import { CanActivate, Router } from "@angular/router"; +import { ApiKeyService } from "jslib-common/abstractions/apiKey.service"; -import { MessagingService } from 'jslib-common/abstractions/messaging.service'; +import { MessagingService } from "jslib-common/abstractions/messaging.service"; @Injectable() export class AuthGuardService implements CanActivate { - constructor(private apiKeyService: ApiKeyService, private router: Router, - private messagingService: MessagingService) { } + constructor( + private apiKeyService: ApiKeyService, + private router: Router, + private messagingService: MessagingService + ) {} - async canActivate() { - const isAuthed = await this.apiKeyService.isAuthenticated(); - if (!isAuthed) { - this.messagingService.send('logout'); - return false; - } - - return true; + async canActivate() { + const isAuthed = await this.apiKeyService.isAuthenticated(); + if (!isAuthed) { + this.messagingService.send("logout"); + return false; } + + return true; + } } diff --git a/src/app/services/launch-guard.service.ts b/src/app/services/launch-guard.service.ts index bc596edf..1199bff9 100644 --- a/src/app/services/launch-guard.service.ts +++ b/src/app/services/launch-guard.service.ts @@ -1,22 +1,19 @@ -import { Injectable } from '@angular/core'; -import { - CanActivate, - Router, -} from '@angular/router'; +import { Injectable } from "@angular/core"; +import { CanActivate, Router } from "@angular/router"; -import { ApiKeyService } from 'jslib-common/abstractions/apiKey.service'; +import { ApiKeyService } from "jslib-common/abstractions/apiKey.service"; @Injectable() export class LaunchGuardService implements CanActivate { - constructor(private apiKeyService: ApiKeyService, private router: Router) { } + constructor(private apiKeyService: ApiKeyService, private router: Router) {} - async canActivate() { - const isAuthed = await this.apiKeyService.isAuthenticated(); - if (!isAuthed) { - return true; - } - - this.router.navigate(['/tabs/dashboard']); - return false; + async canActivate() { + const isAuthed = await this.apiKeyService.isAuthenticated(); + if (!isAuthed) { + return true; } + + this.router.navigate(["/tabs/dashboard"]); + return false; + } } diff --git a/src/app/services/services.module.ts b/src/app/services/services.module.ts index ce45a0bc..774ce526 100644 --- a/src/app/services/services.module.ts +++ b/src/app/services/services.module.ts @@ -1,219 +1,222 @@ -import { - APP_INITIALIZER, - Injector, - NgModule, -} from '@angular/core'; +import { APP_INITIALIZER, Injector, NgModule } from "@angular/core"; -import { ElectronLogService } from 'jslib-electron/services/electronLog.service'; -import { ElectronPlatformUtilsService } from 'jslib-electron/services/electronPlatformUtils.service'; -import { ElectronRendererMessagingService } from 'jslib-electron/services/electronRendererMessaging.service'; -import { ElectronRendererSecureStorageService } from 'jslib-electron/services/electronRendererSecureStorage.service'; -import { ElectronRendererStorageService } from 'jslib-electron/services/electronRendererStorage.service'; +import { ElectronLogService } from "jslib-electron/services/electronLog.service"; +import { ElectronPlatformUtilsService } from "jslib-electron/services/electronPlatformUtils.service"; +import { ElectronRendererMessagingService } from "jslib-electron/services/electronRendererMessaging.service"; +import { ElectronRendererSecureStorageService } from "jslib-electron/services/electronRendererSecureStorage.service"; +import { ElectronRendererStorageService } from "jslib-electron/services/electronRendererStorage.service"; -import { AuthGuardService } from './auth-guard.service'; -import { LaunchGuardService } from './launch-guard.service'; +import { AuthGuardService } from "./auth-guard.service"; +import { LaunchGuardService } from "./launch-guard.service"; -import { ConfigurationService } from '../../services/configuration.service'; -import { I18nService } from '../../services/i18n.service'; -import { SyncService } from '../../services/sync.service'; +import { ConfigurationService } from "../../services/configuration.service"; +import { I18nService } from "../../services/i18n.service"; +import { SyncService } from "../../services/sync.service"; -import { BroadcasterService } from 'jslib-angular/services/broadcaster.service'; -import { JslibServicesModule } from 'jslib-angular/services/jslib-services.module'; -import { ModalService } from 'jslib-angular/services/modal.service'; -import { ValidationService } from 'jslib-angular/services/validation.service'; +import { BroadcasterService } from "jslib-angular/services/broadcaster.service"; +import { JslibServicesModule } from "jslib-angular/services/jslib-services.module"; +import { ModalService } from "jslib-angular/services/modal.service"; +import { ValidationService } from "jslib-angular/services/validation.service"; -import { ApiKeyService } from 'jslib-common/services/apiKey.service'; -import { ConstantsService } from 'jslib-common/services/constants.service'; -import { ContainerService } from 'jslib-common/services/container.service'; +import { ApiKeyService } from "jslib-common/services/apiKey.service"; +import { ConstantsService } from "jslib-common/services/constants.service"; +import { ContainerService } from "jslib-common/services/container.service"; -import { NodeCryptoFunctionService } from 'jslib-node/services/nodeCryptoFunction.service'; +import { NodeCryptoFunctionService } from "jslib-node/services/nodeCryptoFunction.service"; -import { ApiService as ApiServiceAbstraction } from 'jslib-common/abstractions/api.service'; -import { ApiKeyService as ApiKeyServiceAbstraction } from 'jslib-common/abstractions/apiKey.service'; -import { AppIdService as AppIdServiceAbstraction } from 'jslib-common/abstractions/appId.service'; -import { AuthService as AuthServiceAbstraction } from 'jslib-common/abstractions/auth.service'; -import { BroadcasterService as BroadcasterServiceAbstraction } from 'jslib-common/abstractions/broadcaster.service'; -import { CryptoService as CryptoServiceAbstraction } from 'jslib-common/abstractions/crypto.service'; -import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from 'jslib-common/abstractions/cryptoFunction.service'; -import { EnvironmentService as EnvironmentServiceAbstraction } from 'jslib-common/abstractions/environment.service'; -import { I18nService as I18nServiceAbstraction } from 'jslib-common/abstractions/i18n.service'; -import { KeyConnectorService as KeyConnectorServiceAbstraction } from 'jslib-common/abstractions/keyConnector.service'; -import { LogService as LogServiceAbstraction } from 'jslib-common/abstractions/log.service'; -import { MessagingService as MessagingServiceAbstraction } from 'jslib-common/abstractions/messaging.service'; -import { - PasswordGenerationService as PasswordGenerationServiceAbstraction, -} from 'jslib-common/abstractions/passwordGeneration.service'; -import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from 'jslib-common/abstractions/platformUtils.service'; -import { PolicyService as PolicyServiceAbstraction } from 'jslib-common/abstractions/policy.service'; -import { StateService as StateServiceAbstraction } from 'jslib-common/abstractions/state.service'; -import { StorageService as StorageServiceAbstraction } from 'jslib-common/abstractions/storage.service'; -import { TokenService as TokenServiceAbstraction } from 'jslib-common/abstractions/token.service'; -import { UserService as UserServiceAbstraction } from 'jslib-common/abstractions/user.service'; -import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from 'jslib-common/abstractions/vaultTimeout.service'; +import { ApiService as ApiServiceAbstraction } from "jslib-common/abstractions/api.service"; +import { ApiKeyService as ApiKeyServiceAbstraction } from "jslib-common/abstractions/apiKey.service"; +import { AppIdService as AppIdServiceAbstraction } from "jslib-common/abstractions/appId.service"; +import { AuthService as AuthServiceAbstraction } from "jslib-common/abstractions/auth.service"; +import { BroadcasterService as BroadcasterServiceAbstraction } from "jslib-common/abstractions/broadcaster.service"; +import { CryptoService as CryptoServiceAbstraction } from "jslib-common/abstractions/crypto.service"; +import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "jslib-common/abstractions/cryptoFunction.service"; +import { EnvironmentService as EnvironmentServiceAbstraction } from "jslib-common/abstractions/environment.service"; +import { I18nService as I18nServiceAbstraction } from "jslib-common/abstractions/i18n.service"; +import { KeyConnectorService as KeyConnectorServiceAbstraction } from "jslib-common/abstractions/keyConnector.service"; +import { LogService as LogServiceAbstraction } from "jslib-common/abstractions/log.service"; +import { MessagingService as MessagingServiceAbstraction } from "jslib-common/abstractions/messaging.service"; +import { PasswordGenerationService as PasswordGenerationServiceAbstraction } from "jslib-common/abstractions/passwordGeneration.service"; +import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "jslib-common/abstractions/platformUtils.service"; +import { PolicyService as PolicyServiceAbstraction } from "jslib-common/abstractions/policy.service"; +import { StateService as StateServiceAbstraction } from "jslib-common/abstractions/state.service"; +import { StorageService as StorageServiceAbstraction } from "jslib-common/abstractions/storage.service"; +import { TokenService as TokenServiceAbstraction } from "jslib-common/abstractions/token.service"; +import { UserService as UserServiceAbstraction } from "jslib-common/abstractions/user.service"; +import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "jslib-common/abstractions/vaultTimeout.service"; -import { ApiService, refreshToken } from '../../services/api.service'; -import { AuthService } from '../../services/auth.service'; +import { ApiService, refreshToken } from "../../services/api.service"; +import { AuthService } from "../../services/auth.service"; function refreshTokenCallback(injector: Injector) { - return () => { - const apiKeyService = injector.get(ApiKeyServiceAbstraction); - const authService = injector.get(AuthServiceAbstraction); - return refreshToken(apiKeyService, authService); - }; + return () => { + const apiKeyService = injector.get(ApiKeyServiceAbstraction); + const authService = injector.get(AuthServiceAbstraction); + return refreshToken(apiKeyService, authService); + }; } -export function initFactory(environmentService: EnvironmentServiceAbstraction, - i18nService: I18nService, authService: AuthService, platformUtilsService: PlatformUtilsServiceAbstraction, - storageService: StorageServiceAbstraction, userService: UserServiceAbstraction, apiService: ApiServiceAbstraction, - stateService: StateServiceAbstraction, cryptoService: CryptoServiceAbstraction): Function { - return async () => { - await environmentService.setUrlsFromStorage(); - await i18nService.init(); - authService.init(); - const htmlEl = window.document.documentElement; - htmlEl.classList.add('os_' + platformUtilsService.getDeviceString()); - htmlEl.classList.add('locale_' + i18nService.translationLocale); - window.document.title = i18nService.t('bitwardenDirectoryConnector'); +export function initFactory( + environmentService: EnvironmentServiceAbstraction, + i18nService: I18nService, + authService: AuthService, + platformUtilsService: PlatformUtilsServiceAbstraction, + storageService: StorageServiceAbstraction, + userService: UserServiceAbstraction, + apiService: ApiServiceAbstraction, + stateService: StateServiceAbstraction, + cryptoService: CryptoServiceAbstraction +): Function { + return async () => { + await environmentService.setUrlsFromStorage(); + await i18nService.init(); + authService.init(); + const htmlEl = window.document.documentElement; + htmlEl.classList.add("os_" + platformUtilsService.getDeviceString()); + htmlEl.classList.add("locale_" + i18nService.translationLocale); + window.document.title = i18nService.t("bitwardenDirectoryConnector"); - let installAction = null; - const installedVersion = await storageService.get(ConstantsService.installedVersionKey); - const currentVersion = await platformUtilsService.getApplicationVersion(); - if (installedVersion == null) { - installAction = 'install'; - } else if (installedVersion !== currentVersion) { - installAction = 'update'; - } + let installAction = null; + const installedVersion = await storageService.get(ConstantsService.installedVersionKey); + const currentVersion = await platformUtilsService.getApplicationVersion(); + if (installedVersion == null) { + installAction = "install"; + } else if (installedVersion !== currentVersion) { + installAction = "update"; + } - if (installAction != null) { - await storageService.save(ConstantsService.installedVersionKey, currentVersion); - } + if (installAction != null) { + await storageService.save(ConstantsService.installedVersionKey, currentVersion); + } - window.setTimeout(async () => { - if (await userService.isAuthenticated()) { - const profile = await apiService.getProfile(); - stateService.save('profileOrganizations', profile.organizations); - } - }, 500); + window.setTimeout(async () => { + if (await userService.isAuthenticated()) { + const profile = await apiService.getProfile(); + stateService.save("profileOrganizations", profile.organizations); + } + }, 500); - const containerService = new ContainerService(cryptoService); - containerService.attachToWindow(window); - }; + const containerService = new ContainerService(cryptoService); + containerService.attachToWindow(window); + }; } @NgModule({ - imports: [ - JslibServicesModule, - ], - declarations: [], - providers: [ - { - provide: APP_INITIALIZER, - useFactory: initFactory, - deps: [ - EnvironmentServiceAbstraction, - I18nServiceAbstraction, - AuthServiceAbstraction, - PlatformUtilsServiceAbstraction, - StorageServiceAbstraction, - UserServiceAbstraction, - ApiServiceAbstraction, - StateServiceAbstraction, - CryptoServiceAbstraction, - ], - multi: true, - }, - { provide: LogServiceAbstraction, useClass: ElectronLogService, deps: [] }, - { - provide: I18nServiceAbstraction, - useFactory: (window: Window) => new I18nService(window.navigator.language, './locales'), - deps: [ 'WINDOW' ], - }, - { - provide: MessagingServiceAbstraction, - useClass: ElectronRendererMessagingService, - deps: [ BroadcasterServiceAbstraction ], - }, - { provide: StorageServiceAbstraction, useClass: ElectronRendererStorageService }, - { provide: 'SECURE_STORAGE', useClass: ElectronRendererSecureStorageService }, - { - provide: PlatformUtilsServiceAbstraction, - useFactory: (i18nService: I18nServiceAbstraction, messagingService: MessagingServiceAbstraction, - storageService: StorageServiceAbstraction) => new ElectronPlatformUtilsService(i18nService, - messagingService, true, storageService), - deps: [ - I18nServiceAbstraction, - MessagingServiceAbstraction, - StorageServiceAbstraction, - ], - }, - { provide: CryptoFunctionServiceAbstraction, useClass: NodeCryptoFunctionService, deps: [] }, - { - provide: ApiServiceAbstraction, - useFactory: (tokenService: TokenServiceAbstraction, platformUtilsService: PlatformUtilsServiceAbstraction, - environmentService: EnvironmentServiceAbstraction, messagingService: MessagingServiceAbstraction, - injector: Injector) => - new ApiService(tokenService, platformUtilsService, environmentService, refreshTokenCallback(injector), - async (expired: boolean) => messagingService.send('logout', { expired: expired })), - deps: [ - TokenServiceAbstraction, - PlatformUtilsServiceAbstraction, - EnvironmentServiceAbstraction, - MessagingServiceAbstraction, - Injector, - ], - }, - { - provide: ApiKeyServiceAbstraction, - useClass: ApiKeyService, - deps: [ - TokenServiceAbstraction, - StorageServiceAbstraction, - ], - }, - { - provide: AuthServiceAbstraction, - useClass: AuthService, - deps: [ - CryptoServiceAbstraction, - ApiServiceAbstraction, - UserServiceAbstraction, - TokenServiceAbstraction, - AppIdServiceAbstraction, - I18nServiceAbstraction, - PlatformUtilsServiceAbstraction, - MessagingServiceAbstraction, - VaultTimeoutServiceAbstraction, - LogServiceAbstraction, - ApiKeyServiceAbstraction, - CryptoFunctionServiceAbstraction, - EnvironmentServiceAbstraction, - KeyConnectorServiceAbstraction, - ], - }, - { - provide: ConfigurationService, - useClass: ConfigurationService, - deps: [ - StorageServiceAbstraction, - 'SECURE_STORAGE', - ], - }, - { - provide: SyncService, - useClass: SyncService, - deps: [ - ConfigurationService, - LogServiceAbstraction, - CryptoFunctionServiceAbstraction, - ApiServiceAbstraction, - MessagingServiceAbstraction, - I18nServiceAbstraction, - EnvironmentServiceAbstraction, - ], - }, - AuthGuardService, - LaunchGuardService, - ], + imports: [JslibServicesModule], + declarations: [], + providers: [ + { + provide: APP_INITIALIZER, + useFactory: initFactory, + deps: [ + EnvironmentServiceAbstraction, + I18nServiceAbstraction, + AuthServiceAbstraction, + PlatformUtilsServiceAbstraction, + StorageServiceAbstraction, + UserServiceAbstraction, + ApiServiceAbstraction, + StateServiceAbstraction, + CryptoServiceAbstraction, + ], + multi: true, + }, + { provide: LogServiceAbstraction, useClass: ElectronLogService, deps: [] }, + { + provide: I18nServiceAbstraction, + useFactory: (window: Window) => new I18nService(window.navigator.language, "./locales"), + deps: ["WINDOW"], + }, + { + provide: MessagingServiceAbstraction, + useClass: ElectronRendererMessagingService, + deps: [BroadcasterServiceAbstraction], + }, + { provide: StorageServiceAbstraction, useClass: ElectronRendererStorageService }, + { provide: "SECURE_STORAGE", useClass: ElectronRendererSecureStorageService }, + { + provide: PlatformUtilsServiceAbstraction, + useFactory: ( + i18nService: I18nServiceAbstraction, + messagingService: MessagingServiceAbstraction, + storageService: StorageServiceAbstraction + ) => new ElectronPlatformUtilsService(i18nService, messagingService, true, storageService), + deps: [I18nServiceAbstraction, MessagingServiceAbstraction, StorageServiceAbstraction], + }, + { + provide: CryptoFunctionServiceAbstraction, + useClass: NodeCryptoFunctionService, + deps: [], + }, + { + provide: ApiServiceAbstraction, + useFactory: ( + tokenService: TokenServiceAbstraction, + platformUtilsService: PlatformUtilsServiceAbstraction, + environmentService: EnvironmentServiceAbstraction, + messagingService: MessagingServiceAbstraction, + injector: Injector + ) => + new ApiService( + tokenService, + platformUtilsService, + environmentService, + refreshTokenCallback(injector), + async (expired: boolean) => messagingService.send("logout", { expired: expired }) + ), + deps: [ + TokenServiceAbstraction, + PlatformUtilsServiceAbstraction, + EnvironmentServiceAbstraction, + MessagingServiceAbstraction, + Injector, + ], + }, + { + provide: ApiKeyServiceAbstraction, + useClass: ApiKeyService, + deps: [TokenServiceAbstraction, StorageServiceAbstraction], + }, + { + provide: AuthServiceAbstraction, + useClass: AuthService, + deps: [ + CryptoServiceAbstraction, + ApiServiceAbstraction, + UserServiceAbstraction, + TokenServiceAbstraction, + AppIdServiceAbstraction, + I18nServiceAbstraction, + PlatformUtilsServiceAbstraction, + MessagingServiceAbstraction, + VaultTimeoutServiceAbstraction, + LogServiceAbstraction, + ApiKeyServiceAbstraction, + CryptoFunctionServiceAbstraction, + EnvironmentServiceAbstraction, + KeyConnectorServiceAbstraction, + ], + }, + { + provide: ConfigurationService, + useClass: ConfigurationService, + deps: [StorageServiceAbstraction, "SECURE_STORAGE"], + }, + { + provide: SyncService, + useClass: SyncService, + deps: [ + ConfigurationService, + LogServiceAbstraction, + CryptoFunctionServiceAbstraction, + ApiServiceAbstraction, + MessagingServiceAbstraction, + I18nServiceAbstraction, + EnvironmentServiceAbstraction, + ], + }, + AuthGuardService, + LaunchGuardService, + ], }) -export class ServicesModule { -} +export class ServicesModule {} diff --git a/src/app/tabs/dashboard.component.html b/src/app/tabs/dashboard.component.html index f563e0f1..2a21c996 100644 --- a/src/app/tabs/dashboard.component.html +++ b/src/app/tabs/dashboard.component.html @@ -1,99 +1,110 @@
-

{{'sync' | i18n}}

-
-

- {{'lastGroupSync' | i18n}}: - - - {{lastGroupSync | date:'medium'}} -
{{'lastUserSync' | i18n}}: - - - {{lastUserSync | date:'medium'}} -

-

- {{'syncStatus' | i18n}}: - {{'running' | i18n}} - {{'stopped' | i18n}} -

-
- -
- -
- -
-
+

{{ "sync" | i18n }}

+
+

+ {{ "lastGroupSync" | i18n }}: + - + {{ lastGroupSync | date: "medium" }} +
+ {{ "lastUserSync" | i18n }}: + - + {{ lastUserSync | date: "medium" }} +

+

+ {{ "syncStatus" | i18n }}: + {{ "running" | i18n }} + {{ "stopped" | i18n }} +

+
+ +
+ +
+ +
+
-

{{'testing' | i18n}}

-
-

{{'testingDesc' | i18n}}

-
- -
-
- - -
- -
-
-
-

{{'users' | i18n}}

-
    -
  • - - {{u.displayName}} -
  • -
-

{{'noUsers' | i18n}}

-

{{'disabledUsers' | i18n}}

-
    -
  • - - {{u.displayName}} -
  • -
-

{{'noUsers' | i18n}}

-

{{'deletedUsers' | i18n}}

-
    -
  • - - {{u.displayName}} -
  • -
-

{{'noUsers' | i18n}}

-
-
-

{{'groups' | i18n}}

-
    -
  • - - {{g.displayName}} -
      -
    • {{u.displayName}}
    • -
    -
  • -
-

{{'noGroups' | i18n}}

-
-
-
+

{{ "testing" | i18n }}

+
+

{{ "testingDesc" | i18n }}

+
+ +
+
+ +
+ +
+
+
+

{{ "users" | i18n }}

+
    +
  • + + {{ u.displayName }} +
  • +
+

+ {{ "noUsers" | i18n }} +

+

{{ "disabledUsers" | i18n }}

+
    +
  • + + {{ u.displayName }} +
  • +
+

+ {{ "noUsers" | i18n }} +

+

{{ "deletedUsers" | i18n }}

+
    +
  • + + {{ u.displayName }} +
  • +
+

+ {{ "noUsers" | i18n }} +

+
+
+

{{ "groups" | i18n }}

+
    +
  • + + {{ g.displayName }} +
      +
    • + {{ u.displayName }} +
    • +
    +
  • +
+

{{ "noGroups" | i18n }}

+
+
+
+
diff --git a/src/app/tabs/dashboard.component.ts b/src/app/tabs/dashboard.component.ts index 64354ddc..e65b4b1c 100644 --- a/src/app/tabs/dashboard.component.ts +++ b/src/app/tabs/dashboard.component.ts @@ -1,121 +1,128 @@ -import { - ChangeDetectorRef, - Component, - NgZone, - OnDestroy, - OnInit, -} from '@angular/core'; +import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core"; -import { BroadcasterService } from 'jslib-common/abstractions/broadcaster.service'; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { MessagingService } from 'jslib-common/abstractions/messaging.service'; -import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; -import { StateService } from 'jslib-common/abstractions/state.service'; +import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { MessagingService } from "jslib-common/abstractions/messaging.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; +import { StateService } from "jslib-common/abstractions/state.service"; -import { SyncService } from '../../services/sync.service'; +import { SyncService } from "../../services/sync.service"; -import { GroupEntry } from '../../models/groupEntry'; -import { SimResult } from '../../models/simResult'; -import { UserEntry } from '../../models/userEntry'; -import { ConfigurationService } from '../../services/configuration.service'; +import { GroupEntry } from "../../models/groupEntry"; +import { SimResult } from "../../models/simResult"; +import { UserEntry } from "../../models/userEntry"; +import { ConfigurationService } from "../../services/configuration.service"; -import { ConnectorUtils } from '../../utils'; +import { ConnectorUtils } from "../../utils"; -const BroadcasterSubscriptionId = 'DashboardComponent'; +const BroadcasterSubscriptionId = "DashboardComponent"; @Component({ - selector: 'app-dashboard', - templateUrl: 'dashboard.component.html', + selector: "app-dashboard", + templateUrl: "dashboard.component.html", }) export class DashboardComponent implements OnInit, OnDestroy { - simGroups: GroupEntry[]; - simUsers: UserEntry[]; - simEnabledUsers: UserEntry[] = []; - simDisabledUsers: UserEntry[] = []; - simDeletedUsers: UserEntry[] = []; - simPromise: Promise; - simSinceLast: boolean = false; - syncPromise: Promise<[GroupEntry[], UserEntry[]]>; - startPromise: Promise; - lastGroupSync: Date; - lastUserSync: Date; - syncRunning: boolean; + simGroups: GroupEntry[]; + simUsers: UserEntry[]; + simEnabledUsers: UserEntry[] = []; + simDisabledUsers: UserEntry[] = []; + simDeletedUsers: UserEntry[] = []; + simPromise: Promise; + simSinceLast: boolean = false; + syncPromise: Promise<[GroupEntry[], UserEntry[]]>; + startPromise: Promise; + lastGroupSync: Date; + lastUserSync: Date; + syncRunning: boolean; - constructor(private i18nService: I18nService, private syncService: SyncService, - private configurationService: ConfigurationService, private broadcasterService: BroadcasterService, - private ngZone: NgZone, private messagingService: MessagingService, - private platformUtilsService: PlatformUtilsService, private changeDetectorRef: ChangeDetectorRef, - private stateService: StateService) { } + constructor( + private i18nService: I18nService, + private syncService: SyncService, + private configurationService: ConfigurationService, + private broadcasterService: BroadcasterService, + private ngZone: NgZone, + private messagingService: MessagingService, + private platformUtilsService: PlatformUtilsService, + private changeDetectorRef: ChangeDetectorRef, + private stateService: StateService + ) {} - async ngOnInit() { - this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => { - this.ngZone.run(async () => { - switch (message.command) { - case 'dirSyncCompleted': - this.updateLastSync(); - break; - default: - break; - } - - this.changeDetectorRef.detectChanges(); - }); - }); - - this.syncRunning = !!(await this.stateService.get('syncingDir')); - this.updateLastSync(); - } - - async ngOnDestroy() { - this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); - } - - async start() { - this.startPromise = this.syncService.sync(false, false); - await this.startPromise; - this.messagingService.send('scheduleNextDirSync'); - this.syncRunning = true; - this.platformUtilsService.showToast('success', null, this.i18nService.t('syncingStarted')); - } - - async stop() { - this.messagingService.send('cancelDirSync'); - this.syncRunning = false; - this.platformUtilsService.showToast('success', null, this.i18nService.t('syncingStopped')); - } - - async sync() { - this.syncPromise = this.syncService.sync(false, false); - const result = await this.syncPromise; - const groupCount = result[0] != null ? result[0].length : 0; - const userCount = result[1] != null ? result[1].length : 0; - this.platformUtilsService.showToast('success', null, - this.i18nService.t('syncCounts', groupCount.toString(), userCount.toString())); - } - - async simulate() { - this.simGroups = []; - this.simUsers = []; - this.simEnabledUsers = []; - this.simDisabledUsers = []; - this.simDeletedUsers = []; - - try { - this.simPromise = ConnectorUtils.simulate(this.syncService, this.i18nService, this.simSinceLast); - const result = await this.simPromise; - this.simGroups = result.groups; - this.simUsers = result.users; - this.simEnabledUsers = result.enabledUsers; - this.simDisabledUsers = result.disabledUsers; - this.simDeletedUsers = result.deletedUsers; - } catch (e) { - this.simGroups = null; - this.simUsers = null; + async ngOnInit() { + this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => { + this.ngZone.run(async () => { + switch (message.command) { + case "dirSyncCompleted": + this.updateLastSync(); + break; + default: + break; } - } - private async updateLastSync() { - this.lastGroupSync = await this.configurationService.getLastGroupSyncDate(); - this.lastUserSync = await this.configurationService.getLastUserSyncDate(); + this.changeDetectorRef.detectChanges(); + }); + }); + + this.syncRunning = !!(await this.stateService.get("syncingDir")); + this.updateLastSync(); + } + + async ngOnDestroy() { + this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); + } + + async start() { + this.startPromise = this.syncService.sync(false, false); + await this.startPromise; + this.messagingService.send("scheduleNextDirSync"); + this.syncRunning = true; + this.platformUtilsService.showToast("success", null, this.i18nService.t("syncingStarted")); + } + + async stop() { + this.messagingService.send("cancelDirSync"); + this.syncRunning = false; + this.platformUtilsService.showToast("success", null, this.i18nService.t("syncingStopped")); + } + + async sync() { + this.syncPromise = this.syncService.sync(false, false); + const result = await this.syncPromise; + const groupCount = result[0] != null ? result[0].length : 0; + const userCount = result[1] != null ? result[1].length : 0; + this.platformUtilsService.showToast( + "success", + null, + this.i18nService.t("syncCounts", groupCount.toString(), userCount.toString()) + ); + } + + async simulate() { + this.simGroups = []; + this.simUsers = []; + this.simEnabledUsers = []; + this.simDisabledUsers = []; + this.simDeletedUsers = []; + + try { + this.simPromise = ConnectorUtils.simulate( + this.syncService, + this.i18nService, + this.simSinceLast + ); + const result = await this.simPromise; + this.simGroups = result.groups; + this.simUsers = result.users; + this.simEnabledUsers = result.enabledUsers; + this.simDisabledUsers = result.disabledUsers; + this.simDeletedUsers = result.deletedUsers; + } catch (e) { + this.simGroups = null; + this.simUsers = null; } + } + + private async updateLastSync() { + this.lastGroupSync = await this.configurationService.getLastGroupSyncDate(); + this.lastUserSync = await this.configurationService.getLastUserSyncDate(); + } } diff --git a/src/app/tabs/more.component.html b/src/app/tabs/more.component.html index e8759749..14508c02 100644 --- a/src/app/tabs/more.component.html +++ b/src/app/tabs/more.component.html @@ -1,32 +1,38 @@
-
-
-

{{'about' | i18n}}

-
-

- {{'bitwardenDirectoryConnector' | i18n}} -
{{'version' | i18n : version}} -
© Bitwarden Inc. LLC 2015-{{year}} -

- -
-
+
+
+

{{ "about" | i18n }}

+
+

+ {{ "bitwardenDirectoryConnector" | i18n }} +
+ {{ "version" | i18n: version }}
+ © Bitwarden Inc. LLC 2015-{{ year }} +

+ +
-
-
-

{{'other' | i18n}}

-
- - -
-
+
+
+
+

{{ "other" | i18n }}

+
+ + +
+
diff --git a/src/app/tabs/more.component.ts b/src/app/tabs/more.component.ts index ec949c64..d3e4bab8 100644 --- a/src/app/tabs/more.component.ts +++ b/src/app/tabs/more.component.ts @@ -1,75 +1,77 @@ -import { - ChangeDetectorRef, - Component, - NgZone, - OnDestroy, - OnInit, -} from '@angular/core'; +import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core"; -import { BroadcasterService } from 'jslib-common/abstractions/broadcaster.service'; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { MessagingService } from 'jslib-common/abstractions/messaging.service'; -import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; +import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { MessagingService } from "jslib-common/abstractions/messaging.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; -import { ConfigurationService } from '../../services/configuration.service'; +import { ConfigurationService } from "../../services/configuration.service"; -const BroadcasterSubscriptionId = 'MoreComponent'; +const BroadcasterSubscriptionId = "MoreComponent"; @Component({ - selector: 'app-more', - templateUrl: 'more.component.html', + selector: "app-more", + templateUrl: "more.component.html", }) export class MoreComponent implements OnInit { - version: string; - year: string; - checkingForUpdate = false; + version: string; + year: string; + checkingForUpdate = false; - constructor(private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, - private messagingService: MessagingService, private configurationService: ConfigurationService, - private broadcasterService: BroadcasterService, - private ngZone: NgZone, private changeDetectorRef: ChangeDetectorRef) { } + constructor( + private platformUtilsService: PlatformUtilsService, + private i18nService: I18nService, + private messagingService: MessagingService, + private configurationService: ConfigurationService, + private broadcasterService: BroadcasterService, + private ngZone: NgZone, + private changeDetectorRef: ChangeDetectorRef + ) {} - async ngOnInit() { - this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => { - this.ngZone.run(async () => { - switch (message.command) { - case 'checkingForUpdate': - this.checkingForUpdate = true; - break; - case 'doneCheckingForUpdate': - this.checkingForUpdate = false; - break; - default: - break; - } - - this.changeDetectorRef.detectChanges(); - }); - }); - - this.year = new Date().getFullYear().toString(); - this.version = await this.platformUtilsService.getApplicationVersion(); - } - - ngOnDestroy() { - this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); - } - - update() { - this.messagingService.send('checkForUpdate'); - } - - async logOut() { - const confirmed = await this.platformUtilsService.showDialog( - this.i18nService.t('logOutConfirmation'), this.i18nService.t('logOut'), - this.i18nService.t('yes'), this.i18nService.t('cancel')); - if (confirmed) { - this.messagingService.send('logout'); + async ngOnInit() { + this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => { + this.ngZone.run(async () => { + switch (message.command) { + case "checkingForUpdate": + this.checkingForUpdate = true; + break; + case "doneCheckingForUpdate": + this.checkingForUpdate = false; + break; + default: + break; } - } - async clearCache() { - await this.configurationService.clearStatefulSettings(true); - this.platformUtilsService.showToast('success', null, this.i18nService.t('syncCacheCleared')); + this.changeDetectorRef.detectChanges(); + }); + }); + + this.year = new Date().getFullYear().toString(); + this.version = await this.platformUtilsService.getApplicationVersion(); + } + + ngOnDestroy() { + this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); + } + + update() { + this.messagingService.send("checkForUpdate"); + } + + async logOut() { + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t("logOutConfirmation"), + this.i18nService.t("logOut"), + this.i18nService.t("yes"), + this.i18nService.t("cancel") + ); + if (confirmed) { + this.messagingService.send("logout"); } + } + + async clearCache() { + await this.configurationService.clearStatefulSettings(true); + this.platformUtilsService.showToast("success", null, this.i18nService.t("syncCacheCleared")); + } } diff --git a/src/app/tabs/settings.component.html b/src/app/tabs/settings.component.html index a07f6bb6..89021038 100644 --- a/src/app/tabs/settings.component.html +++ b/src/app/tabs/settings.component.html @@ -1,425 +1,757 @@
-
-
-

{{'directory' | i18n}}

-
-
- - -
-
-
- - - {{'ex' | i18n}} ad.company.com -
-
- - - {{'ex' | i18n}} 389 -
-
- - - {{'ex' | i18n}} dc=company,dc=com -
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
-
- - -
-
- - -
-
-
-

{{'ldapTlsUntrustedDesc' | i18n}}

-
- - - -
-
-
-

{{'ldapSslUntrustedDesc' | i18n}}

-
- - - -
-
- - - -
-
- - - -
-
-
-
- - -
-
-
-
-
- - -
-
-
-
- - - {{'ex' | i18n}} company\admin - {{'ex' | i18n}} - cn=admin,dc=company,dc=com -
-
- -
- -
- -
-
-
-
-
-
-
- - -
-
- - - {{'ex' | i18n}} companyad.onmicrosoft.com -
-
- - -
-
- -
- -
- -
-
-
-
-
-
- - - {{'ex' | i18n}} https://mycompany.okta.com/ -
-
- -
- -
- -
-
-
-
-
-
- - -
-
- -
- -
- -
-
-
-
- - -
-
-
-
- - - {{'ex' | i18n}} company.com -
-
- - - {{'ex' | i18n}} admin@company.com -
-
- - - {{'ex' | i18n}} 39204722352 -
-
- - - {{'ex' | i18n}} My Project-jksd3jd223.json -
-
- - -
-
- - -
-
+
+
+

{{ "directory" | i18n }}

+
+
+ + +
+
+
+ + + {{ "ex" | i18n }} ad.company.com +
+
+ + + {{ "ex" | i18n }} 389 +
+
+ + + {{ "ex" | i18n }} dc=company,dc=com +
+
+
+ +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+ + +
+
+ + +
+
+
+

{{ "ldapTlsUntrustedDesc" | i18n }}

+
+ + + +
+
+
+

{{ "ldapSslUntrustedDesc" | i18n }}

+
+ + + +
+
+ + + +
+
+ + + +
+
+
+
+ + +
+
+
+
+
+ + +
+
+
+
+ + + {{ "ex" | i18n }} company\admin + {{ "ex" | i18n }} cn=admin,dc=company,dc=com +
+
+ +
+ +
+ +
+
+
+
+
+
+
+ + +
+
+ + + {{ "ex" | i18n }} companyad.onmicrosoft.com +
+
+ + +
+
+ +
+ +
+ +
+
+
+
+
+
+ + + {{ "ex" | i18n }} https://mycompany.okta.com/ +
+
+ +
+ +
+ +
+
+
+
+
+
+ + +
+
+ +
+ +
+ +
+
+
+
+ + +
+
+
+
+ + + {{ "ex" | i18n }} company.com +
+
+ + + {{ "ex" | i18n }} admin@company.com +
+
+ + + {{ "ex" | i18n }} 39204722352 +
+
+ + + {{ "ex" | i18n }} My Project-jksd3jd223.json +
+
+ + +
+
+ + +
+
+
+
+
+
+
+

{{ "sync" | i18n }}

+
+
+ + + {{ "intervalMin" | i18n }} +
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+ + + {{ "ex" | i18n }} uniqueMember +
+
+ + + {{ "ex" | i18n }} whenCreated +
+
+ + + {{ "ex" | i18n }} whenChanged +
+
+
+
+
+
+ + +
+
+
+
+ + + {{ "ex" | i18n }} accountName +
+
+ + + {{ "ex" | i18n }} @company.com +
+
-
-
-
-

{{'sync' | i18n}}

-
-
- - - {{'intervalMin' | i18n}} -
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
-
- - - {{'ex' | i18n}} uniqueMember -
-
- - - {{'ex' | i18n}} whenCreated -
-
- - - {{'ex' | i18n}} whenChanged -
-
-
-
-
-
- - -
-
-
-
- - - {{'ex' | i18n}} accountName -
-
- - - {{'ex' | i18n}} @company.com -
-
-
- -
-
- - -
-
-
-
- - - {{'ex' | i18n}} - (&(givenName=John)(|(l=Dallas)(l=Austin))) - {{'ex' | i18n}} - exclude:joe@company.com - {{'ex' | i18n}} - exclude:joe@company.com | profile.firstName eq "John" - {{'ex' | i18n}} - exclude:joe@company.com | orgName=Engineering -
-
- - - {{'ex' | i18n}} CN=Users -
-
-
- - - {{'ex' | i18n}} inetOrgPerson -
-
- - - {{'ex' | i18n}} mail -
-
-
- -
-
- - -
-
-
-
- - - {{'ex' | i18n}} - (&!(name=Sales*)!(name=IT*)) - {{'ex' | i18n}} - include:Sales,IT - {{'ex' | i18n}} - include:Sales,IT | type eq "APP_GROUP" - {{'ex' | i18n}} - include:Sales,IT -
-
- - - {{'ex' | i18n}} CN=Groups - {{'ex' | i18n}} CN=Users -
-
-
- - - {{'ex' | i18n}} groupOfUniqueNames -
-
- - - {{'ex' | i18n}} name -
-
-
-
+
+
+ + +
+
+
+ + + {{ "ex" | i18n }} (&(givenName=John)(|(l=Dallas)(l=Austin))) + {{ "ex" | i18n }} exclude:joe@company.com + {{ "ex" | i18n }} exclude:joe@company.com | profile.firstName eq "John" + {{ "ex" | i18n }} exclude:joe@company.com | orgName=Engineering +
+
+ + + {{ "ex" | i18n }} CN=Users +
+
+
+ + + {{ "ex" | i18n }} inetOrgPerson +
+
+ + + {{ "ex" | i18n }} mail +
+
+
+ +
+
+ + +
+
+
+
+ + + {{ "ex" | i18n }} (&!(name=Sales*)!(name=IT*)) + {{ "ex" | i18n }} include:Sales,IT + {{ "ex" | i18n }} include:Sales,IT | type eq "APP_GROUP" + {{ "ex" | i18n }} include:Sales,IT +
+
+ + + {{ "ex" | i18n }} CN=Groups + {{ "ex" | i18n }} CN=Users +
+
+
+ + + {{ "ex" | i18n }} groupOfUniqueNames +
+
+ + + {{ "ex" | i18n }} name +
+
+
+
+
diff --git a/src/app/tabs/settings.component.ts b/src/app/tabs/settings.component.ts index 01a20664..2c1be5fe 100644 --- a/src/app/tabs/settings.component.ts +++ b/src/app/tabs/settings.component.ts @@ -1,156 +1,162 @@ -import { - ChangeDetectorRef, - Component, - NgZone, - OnDestroy, - OnInit, -} from '@angular/core'; +import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core"; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { LogService } from 'jslib-common/abstractions/log.service'; -import { StateService } from 'jslib-common/abstractions/state.service'; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { LogService } from "jslib-common/abstractions/log.service"; +import { StateService } from "jslib-common/abstractions/state.service"; -import { ProfileOrganizationResponse } from 'jslib-common/models/response/profileOrganizationResponse'; +import { ProfileOrganizationResponse } from "jslib-common/models/response/profileOrganizationResponse"; -import { ConfigurationService } from '../../services/configuration.service'; +import { ConfigurationService } from "../../services/configuration.service"; -import { DirectoryType } from '../../enums/directoryType'; +import { DirectoryType } from "../../enums/directoryType"; -import { AzureConfiguration } from '../../models/azureConfiguration'; -import { GSuiteConfiguration } from '../../models/gsuiteConfiguration'; -import { LdapConfiguration } from '../../models/ldapConfiguration'; -import { OktaConfiguration } from '../../models/oktaConfiguration'; -import { OneLoginConfiguration } from '../../models/oneLoginConfiguration'; -import { SyncConfiguration } from '../../models/syncConfiguration'; +import { AzureConfiguration } from "../../models/azureConfiguration"; +import { GSuiteConfiguration } from "../../models/gsuiteConfiguration"; +import { LdapConfiguration } from "../../models/ldapConfiguration"; +import { OktaConfiguration } from "../../models/oktaConfiguration"; +import { OneLoginConfiguration } from "../../models/oneLoginConfiguration"; +import { SyncConfiguration } from "../../models/syncConfiguration"; -import { ConnectorUtils } from '../../utils'; +import { ConnectorUtils } from "../../utils"; @Component({ - selector: 'app-settings', - templateUrl: 'settings.component.html', + selector: "app-settings", + templateUrl: "settings.component.html", }) export class SettingsComponent implements OnInit, OnDestroy { - directory: DirectoryType; - directoryType = DirectoryType; - ldap = new LdapConfiguration(); - gsuite = new GSuiteConfiguration(); - azure = new AzureConfiguration(); - okta = new OktaConfiguration(); - oneLogin = new OneLoginConfiguration(); - sync = new SyncConfiguration(); - directoryOptions: any[]; - showLdapPassword: boolean = false; - showAzureKey: boolean = false; - showOktaKey: boolean = false; - showOneLoginSecret: boolean = false; + directory: DirectoryType; + directoryType = DirectoryType; + ldap = new LdapConfiguration(); + gsuite = new GSuiteConfiguration(); + azure = new AzureConfiguration(); + okta = new OktaConfiguration(); + oneLogin = new OneLoginConfiguration(); + sync = new SyncConfiguration(); + directoryOptions: any[]; + showLdapPassword: boolean = false; + showAzureKey: boolean = false; + showOktaKey: boolean = false; + showOneLoginSecret: boolean = false; - constructor(private i18nService: I18nService, private configurationService: ConfigurationService, - private changeDetectorRef: ChangeDetectorRef, private ngZone: NgZone, - private stateService: StateService, private logService: LogService) { - this.directoryOptions = [ - { name: i18nService.t('select'), value: null }, - { name: 'Active Directory / LDAP', value: DirectoryType.Ldap }, - { name: 'Azure Active Directory', value: DirectoryType.AzureActiveDirectory }, - { name: 'G Suite (Google)', value: DirectoryType.GSuite }, - { name: 'Okta', value: DirectoryType.Okta }, - { name: 'OneLogin', value: DirectoryType.OneLogin }, - ]; + constructor( + private i18nService: I18nService, + private configurationService: ConfigurationService, + private changeDetectorRef: ChangeDetectorRef, + private ngZone: NgZone, + private stateService: StateService, + private logService: LogService + ) { + this.directoryOptions = [ + { name: i18nService.t("select"), value: null }, + { name: "Active Directory / LDAP", value: DirectoryType.Ldap }, + { name: "Azure Active Directory", value: DirectoryType.AzureActiveDirectory }, + { name: "G Suite (Google)", value: DirectoryType.GSuite }, + { name: "Okta", value: DirectoryType.Okta }, + { name: "OneLogin", value: DirectoryType.OneLogin }, + ]; + } + + async ngOnInit() { + this.directory = await this.configurationService.getDirectoryType(); + this.ldap = + (await this.configurationService.getDirectory(DirectoryType.Ldap)) || + this.ldap; + this.gsuite = + (await this.configurationService.getDirectory(DirectoryType.GSuite)) || + this.gsuite; + this.azure = + (await this.configurationService.getDirectory( + DirectoryType.AzureActiveDirectory + )) || this.azure; + this.okta = + (await this.configurationService.getDirectory(DirectoryType.Okta)) || + this.okta; + this.oneLogin = + (await this.configurationService.getDirectory( + DirectoryType.OneLogin + )) || this.oneLogin; + this.sync = (await this.configurationService.getSync()) || this.sync; + } + + async ngOnDestroy() { + await this.submit(); + } + + async submit() { + ConnectorUtils.adjustConfigForSave(this.ldap, this.sync); + if (this.ldap != null && this.ldap.ad) { + this.ldap.pagedSearch = true; + } + await this.configurationService.saveDirectoryType(this.directory); + await this.configurationService.saveDirectory(DirectoryType.Ldap, this.ldap); + await this.configurationService.saveDirectory(DirectoryType.GSuite, this.gsuite); + await this.configurationService.saveDirectory(DirectoryType.AzureActiveDirectory, this.azure); + await this.configurationService.saveDirectory(DirectoryType.Okta, this.okta); + await this.configurationService.saveDirectory(DirectoryType.OneLogin, this.oneLogin); + await this.configurationService.saveSync(this.sync); + } + + parseKeyFile() { + const filePicker = document.getElementById("keyFile") as HTMLInputElement; + if (filePicker.files == null || filePicker.files.length < 0) { + return; } - async ngOnInit() { - this.directory = await this.configurationService.getDirectoryType(); - this.ldap = (await this.configurationService.getDirectory(DirectoryType.Ldap)) || - this.ldap; - this.gsuite = (await this.configurationService.getDirectory(DirectoryType.GSuite)) || - this.gsuite; - this.azure = (await this.configurationService.getDirectory( - DirectoryType.AzureActiveDirectory)) || this.azure; - this.okta = (await this.configurationService.getDirectory( - DirectoryType.Okta)) || this.okta; - this.oneLogin = (await this.configurationService.getDirectory( - DirectoryType.OneLogin)) || this.oneLogin; - this.sync = (await this.configurationService.getSync()) || this.sync; - } - - async ngOnDestroy() { - await this.submit(); - } - - async submit() { - ConnectorUtils.adjustConfigForSave(this.ldap, this.sync); - if (this.ldap != null && this.ldap.ad) { - this.ldap.pagedSearch = true; + const reader = new FileReader(); + reader.readAsText(filePicker.files[0], "utf-8"); + reader.onload = (evt) => { + this.ngZone.run(async () => { + try { + const result = JSON.parse((evt.target as FileReader).result as string); + if (result.client_email != null && result.private_key != null) { + this.gsuite.clientEmail = result.client_email; + this.gsuite.privateKey = result.private_key; + } + } catch (e) { + this.logService.error(e); } - await this.configurationService.saveDirectoryType(this.directory); - await this.configurationService.saveDirectory(DirectoryType.Ldap, this.ldap); - await this.configurationService.saveDirectory(DirectoryType.GSuite, this.gsuite); - await this.configurationService.saveDirectory(DirectoryType.AzureActiveDirectory, this.azure); - await this.configurationService.saveDirectory(DirectoryType.Okta, this.okta); - await this.configurationService.saveDirectory(DirectoryType.OneLogin, this.oneLogin); - await this.configurationService.saveSync(this.sync); + this.changeDetectorRef.detectChanges(); + }); + + // reset file input + // ref: https://stackoverflow.com/a/20552042 + filePicker.type = ""; + filePicker.type = "file"; + filePicker.value = ""; + }; + } + + setSslPath(id: string) { + const filePicker = document.getElementById(id + "_file") as HTMLInputElement; + if (filePicker.files == null || filePicker.files.length < 0) { + return; } - parseKeyFile() { - const filePicker = (document.getElementById('keyFile') as HTMLInputElement); - if (filePicker.files == null || filePicker.files.length < 0) { - return; - } + (this.ldap as any)[id] = filePicker.files[0].path; + // reset file input + // ref: https://stackoverflow.com/a/20552042 + filePicker.type = ""; + filePicker.type = "file"; + filePicker.value = ""; + } - const reader = new FileReader(); - reader.readAsText(filePicker.files[0], 'utf-8'); - reader.onload = evt => { - this.ngZone.run(async () => { - try { - const result = JSON.parse((evt.target as FileReader).result as string); - if (result.client_email != null && result.private_key != null) { - this.gsuite.clientEmail = result.client_email; - this.gsuite.privateKey = result.private_key; - } - } catch (e) { - this.logService.error(e); - } - this.changeDetectorRef.detectChanges(); - }); + toggleLdapPassword() { + this.showLdapPassword = !this.showLdapPassword; + document.getElementById("password").focus(); + } - // reset file input - // ref: https://stackoverflow.com/a/20552042 - filePicker.type = ''; - filePicker.type = 'file'; - filePicker.value = ''; - }; - } + toggleAzureKey() { + this.showAzureKey = !this.showAzureKey; + document.getElementById("secretKey").focus(); + } - setSslPath(id: string) { - const filePicker = (document.getElementById(id + '_file') as HTMLInputElement); - if (filePicker.files == null || filePicker.files.length < 0) { - return; - } + toggleOktaKey() { + this.showOktaKey = !this.showOktaKey; + document.getElementById("oktaToken").focus(); + } - (this.ldap as any)[id] = filePicker.files[0].path; - // reset file input - // ref: https://stackoverflow.com/a/20552042 - filePicker.type = ''; - filePicker.type = 'file'; - filePicker.value = ''; - } - - toggleLdapPassword() { - this.showLdapPassword = !this.showLdapPassword; - document.getElementById('password').focus(); - } - - toggleAzureKey() { - this.showAzureKey = !this.showAzureKey; - document.getElementById('secretKey').focus(); - } - - toggleOktaKey() { - this.showOktaKey = !this.showOktaKey; - document.getElementById('oktaToken').focus(); - } - - toggleOneLoginSecret() { - this.showOneLoginSecret = !this.showOneLoginSecret; - document.getElementById('oneLoginClientSecret').focus(); - } + toggleOneLoginSecret() { + this.showOneLoginSecret = !this.showOneLoginSecret; + document.getElementById("oneLoginClientSecret").focus(); + } } diff --git a/src/app/tabs/tabs.component.html b/src/app/tabs/tabs.component.html index f118cd8d..aef2dcc2 100644 --- a/src/app/tabs/tabs.component.html +++ b/src/app/tabs/tabs.component.html @@ -1,23 +1,23 @@ diff --git a/src/app/tabs/tabs.component.ts b/src/app/tabs/tabs.component.ts index 8c5df545..ea06809e 100644 --- a/src/app/tabs/tabs.component.ts +++ b/src/app/tabs/tabs.component.ts @@ -1,7 +1,7 @@ -import { Component } from '@angular/core'; +import { Component } from "@angular/core"; @Component({ - selector: 'app-tabs', - templateUrl: 'tabs.component.html', + selector: "app-tabs", + templateUrl: "tabs.component.html", }) -export class TabsComponent { } +export class TabsComponent {} diff --git a/src/bwdc.ts b/src/bwdc.ts index bff0f028..7f2a8f46 100644 --- a/src/bwdc.ts +++ b/src/bwdc.ts @@ -1,188 +1,286 @@ -import * as fs from 'fs'; -import * as path from 'path'; +import * as fs from "fs"; +import * as path from "path"; -import { LogLevelType } from 'jslib-common/enums/logLevelType'; +import { LogLevelType } from "jslib-common/enums/logLevelType"; -import { AuthService } from './services/auth.service'; +import { AuthService } from "./services/auth.service"; -import { ConfigurationService } from './services/configuration.service'; -import { I18nService } from './services/i18n.service'; -import { KeytarSecureStorageService } from './services/keytarSecureStorage.service'; -import { LowdbStorageService } from './services/lowdbStorage.service'; -import { NodeApiService } from './services/nodeApi.service'; -import { SyncService } from './services/sync.service'; +import { ConfigurationService } from "./services/configuration.service"; +import { I18nService } from "./services/i18n.service"; +import { KeytarSecureStorageService } from "./services/keytarSecureStorage.service"; +import { LowdbStorageService } from "./services/lowdbStorage.service"; +import { NodeApiService } from "./services/nodeApi.service"; +import { SyncService } from "./services/sync.service"; -import { CliPlatformUtilsService } from 'jslib-node/cli/services/cliPlatformUtils.service'; -import { ConsoleLogService } from 'jslib-node/cli/services/consoleLog.service'; -import { NodeCryptoFunctionService } from 'jslib-node/services/nodeCryptoFunction.service'; +import { CliPlatformUtilsService } from "jslib-node/cli/services/cliPlatformUtils.service"; +import { ConsoleLogService } from "jslib-node/cli/services/consoleLog.service"; +import { NodeCryptoFunctionService } from "jslib-node/services/nodeCryptoFunction.service"; -import { ApiKeyService } from 'jslib-common/services/apiKey.service'; -import { AppIdService } from 'jslib-common/services/appId.service'; -import { CipherService } from 'jslib-common/services/cipher.service'; -import { CollectionService } from 'jslib-common/services/collection.service'; -import { ConstantsService } from 'jslib-common/services/constants.service'; -import { ContainerService } from 'jslib-common/services/container.service'; -import { CryptoService } from 'jslib-common/services/crypto.service'; -import { EnvironmentService } from 'jslib-common/services/environment.service'; -import { FileUploadService } from 'jslib-common/services/fileUpload.service'; -import { FolderService } from 'jslib-common/services/folder.service'; -import { KeyConnectorService } from 'jslib-common/services/keyConnector.service'; -import { NoopMessagingService } from 'jslib-common/services/noopMessaging.service'; -import { PasswordGenerationService } from 'jslib-common/services/passwordGeneration.service'; -import { PolicyService } from 'jslib-common/services/policy.service'; -import { SearchService } from 'jslib-common/services/search.service'; -import { SendService } from 'jslib-common/services/send.service'; -import { SettingsService } from 'jslib-common/services/settings.service'; -import { SyncService as LoginSyncService } from 'jslib-common/services/sync.service'; -import { TokenService } from 'jslib-common/services/token.service'; -import { UserService } from 'jslib-common/services/user.service'; +import { ApiKeyService } from "jslib-common/services/apiKey.service"; +import { AppIdService } from "jslib-common/services/appId.service"; +import { CipherService } from "jslib-common/services/cipher.service"; +import { CollectionService } from "jslib-common/services/collection.service"; +import { ConstantsService } from "jslib-common/services/constants.service"; +import { ContainerService } from "jslib-common/services/container.service"; +import { CryptoService } from "jslib-common/services/crypto.service"; +import { EnvironmentService } from "jslib-common/services/environment.service"; +import { FileUploadService } from "jslib-common/services/fileUpload.service"; +import { FolderService } from "jslib-common/services/folder.service"; +import { KeyConnectorService } from "jslib-common/services/keyConnector.service"; +import { NoopMessagingService } from "jslib-common/services/noopMessaging.service"; +import { PasswordGenerationService } from "jslib-common/services/passwordGeneration.service"; +import { PolicyService } from "jslib-common/services/policy.service"; +import { SearchService } from "jslib-common/services/search.service"; +import { SendService } from "jslib-common/services/send.service"; +import { SettingsService } from "jslib-common/services/settings.service"; +import { SyncService as LoginSyncService } from "jslib-common/services/sync.service"; +import { TokenService } from "jslib-common/services/token.service"; +import { UserService } from "jslib-common/services/user.service"; -import { StorageService as StorageServiceAbstraction } from 'jslib-common/abstractions/storage.service'; +import { StorageService as StorageServiceAbstraction } from "jslib-common/abstractions/storage.service"; -import { Program } from './program'; -import { refreshToken } from './services/api.service'; +import { Program } from "./program"; +import { refreshToken } from "./services/api.service"; // tslint:disable-next-line -const packageJson = require('./package.json'); +const packageJson = require("./package.json"); export let searchService: SearchService = null; export class Main { - dataFilePath: string; - logService: ConsoleLogService; - messagingService: NoopMessagingService; - storageService: LowdbStorageService; - secureStorageService: StorageServiceAbstraction; - i18nService: I18nService; - platformUtilsService: CliPlatformUtilsService; - constantsService: ConstantsService; - cryptoService: CryptoService; - tokenService: TokenService; - appIdService: AppIdService; - apiService: NodeApiService; - environmentService: EnvironmentService; - apiKeyService: ApiKeyService; - userService: UserService; - containerService: ContainerService; - cryptoFunctionService: NodeCryptoFunctionService; - authService: AuthService; - configurationService: ConfigurationService; - collectionService: CollectionService; - cipherService: CipherService; - fileUploadService: FileUploadService; - folderService: FolderService; - searchService: SearchService; - sendService: SendService; - settingsService: SettingsService; - syncService: SyncService; - passwordGenerationService: PasswordGenerationService; - policyService: PolicyService; - loginSyncService: LoginSyncService; - keyConnectorService: KeyConnectorService; - program: Program; + dataFilePath: string; + logService: ConsoleLogService; + messagingService: NoopMessagingService; + storageService: LowdbStorageService; + secureStorageService: StorageServiceAbstraction; + i18nService: I18nService; + platformUtilsService: CliPlatformUtilsService; + constantsService: ConstantsService; + cryptoService: CryptoService; + tokenService: TokenService; + appIdService: AppIdService; + apiService: NodeApiService; + environmentService: EnvironmentService; + apiKeyService: ApiKeyService; + userService: UserService; + containerService: ContainerService; + cryptoFunctionService: NodeCryptoFunctionService; + authService: AuthService; + configurationService: ConfigurationService; + collectionService: CollectionService; + cipherService: CipherService; + fileUploadService: FileUploadService; + folderService: FolderService; + searchService: SearchService; + sendService: SendService; + settingsService: SettingsService; + syncService: SyncService; + passwordGenerationService: PasswordGenerationService; + policyService: PolicyService; + loginSyncService: LoginSyncService; + keyConnectorService: KeyConnectorService; + program: Program; - constructor() { - const applicationName = 'Bitwarden Directory Connector'; - if (process.env.BITWARDENCLI_CONNECTOR_APPDATA_DIR) { - this.dataFilePath = path.resolve(process.env.BITWARDENCLI_CONNECTOR_APPDATA_DIR); - } else if (process.env.BITWARDEN_CONNECTOR_APPDATA_DIR) { - this.dataFilePath = path.resolve(process.env.BITWARDEN_CONNECTOR_APPDATA_DIR); - } else if (fs.existsSync(path.join(__dirname, 'bitwarden-connector-appdata'))) { - this.dataFilePath = path.join(__dirname, 'bitwarden-connector-appdata'); - } else if (process.platform === 'darwin') { - this.dataFilePath = path.join(process.env.HOME, 'Library/Application Support/' + applicationName); - } else if (process.platform === 'win32') { - this.dataFilePath = path.join(process.env.APPDATA, applicationName); - } else if (process.env.XDG_CONFIG_HOME) { - this.dataFilePath = path.join(process.env.XDG_CONFIG_HOME, applicationName); - } else { - this.dataFilePath = path.join(process.env.HOME, '.config/' + applicationName); - } - - const plaintextSecrets = process.env.BITWARDENCLI_CONNECTOR_PLAINTEXT_SECRETS === 'true'; - this.i18nService = new I18nService('en', './locales'); - this.platformUtilsService = new CliPlatformUtilsService('connector', packageJson); - this.logService = new ConsoleLogService(this.platformUtilsService.isDev(), - level => process.env.BITWARDENCLI_CONNECTOR_DEBUG !== 'true' && level <= LogLevelType.Info); - this.cryptoFunctionService = new NodeCryptoFunctionService(); - this.storageService = new LowdbStorageService(this.logService, null, this.dataFilePath, false, true); - this.secureStorageService = plaintextSecrets ? - this.storageService : new KeytarSecureStorageService(applicationName); - this.cryptoService = new CryptoService(this.storageService, this.secureStorageService, - this.cryptoFunctionService, this.platformUtilsService, this.logService); - this.appIdService = new AppIdService(this.storageService); - this.tokenService = new TokenService(this.storageService); - this.messagingService = new NoopMessagingService(); - this.environmentService = new EnvironmentService(this.storageService); - this.apiService = new NodeApiService(this.tokenService, this.platformUtilsService, this.environmentService, - () => refreshToken(this.apiKeyService, this.authService), async (expired: boolean) => await this.logout(), - 'Bitwarden_DC/' + this.platformUtilsService.getApplicationVersion() + - ' (' + this.platformUtilsService.getDeviceString().toUpperCase() + ')', (clientId, clientSecret) => - this.authService.logInApiKey(clientId, clientSecret)); - this.apiKeyService = new ApiKeyService(this.tokenService, this.storageService); - this.userService = new UserService(this.tokenService, this.storageService); - this.containerService = new ContainerService(this.cryptoService); - this.keyConnectorService = new KeyConnectorService(this.storageService, this.userService, this.cryptoService, - this.apiService, this.tokenService, this.logService); - this.authService = new AuthService(this.cryptoService, this.apiService, this.userService, this.tokenService, - this.appIdService, this.i18nService, this.platformUtilsService, this.messagingService, null, - this.logService, this.apiKeyService, this.cryptoFunctionService, this.environmentService, this.keyConnectorService); - this.configurationService = new ConfigurationService(this.storageService, this.secureStorageService, - process.env.BITWARDENCLI_CONNECTOR_PLAINTEXT_SECRETS !== 'true'); - this.syncService = new SyncService(this.configurationService, this.logService, this.cryptoFunctionService, - this.apiService, this.messagingService, this.i18nService, this.environmentService); - this.passwordGenerationService = new PasswordGenerationService(this.cryptoService, this.storageService, null); - this.policyService = new PolicyService(this.userService, this.storageService, this.apiService); - this.settingsService = new SettingsService(this.userService, this.storageService); - this.fileUploadService = new FileUploadService(this.logService, this.apiService); - this.cipherService = new CipherService(this.cryptoService, this.userService, this.settingsService, - this.apiService, this.fileUploadService, this.storageService, this.i18nService, () => searchService, - this.logService); - this.searchService = new SearchService(this.cipherService, this.logService, this.i18nService); - this.folderService = new FolderService(this.cryptoService, this.userService, this.apiService, - this.storageService, this.i18nService, this.cipherService); - this.collectionService = new CollectionService(this.cryptoService, this.userService, this.storageService, - this.i18nService); - this.sendService = new SendService(this.cryptoService, this.userService, this.apiService, this.fileUploadService, this.storageService, - this.i18nService, this.cryptoFunctionService); - - this.loginSyncService = new LoginSyncService(this.userService, this.apiService, this.settingsService, - this.folderService, this.cipherService, this.cryptoService, this.collectionService, this.storageService, - this.messagingService, this.policyService, this.sendService, this.logService, this.tokenService, - this.keyConnectorService, async (expired: boolean) => this.messagingService.send('logout', { expired: expired })); - - this.program = new Program(this); + constructor() { + const applicationName = "Bitwarden Directory Connector"; + if (process.env.BITWARDENCLI_CONNECTOR_APPDATA_DIR) { + this.dataFilePath = path.resolve(process.env.BITWARDENCLI_CONNECTOR_APPDATA_DIR); + } else if (process.env.BITWARDEN_CONNECTOR_APPDATA_DIR) { + this.dataFilePath = path.resolve(process.env.BITWARDEN_CONNECTOR_APPDATA_DIR); + } else if (fs.existsSync(path.join(__dirname, "bitwarden-connector-appdata"))) { + this.dataFilePath = path.join(__dirname, "bitwarden-connector-appdata"); + } else if (process.platform === "darwin") { + this.dataFilePath = path.join( + process.env.HOME, + "Library/Application Support/" + applicationName + ); + } else if (process.platform === "win32") { + this.dataFilePath = path.join(process.env.APPDATA, applicationName); + } else if (process.env.XDG_CONFIG_HOME) { + this.dataFilePath = path.join(process.env.XDG_CONFIG_HOME, applicationName); + } else { + this.dataFilePath = path.join(process.env.HOME, ".config/" + applicationName); } - async run() { - await this.init(); - this.program.run(); - } + const plaintextSecrets = process.env.BITWARDENCLI_CONNECTOR_PLAINTEXT_SECRETS === "true"; + this.i18nService = new I18nService("en", "./locales"); + this.platformUtilsService = new CliPlatformUtilsService("connector", packageJson); + this.logService = new ConsoleLogService( + this.platformUtilsService.isDev(), + (level) => process.env.BITWARDENCLI_CONNECTOR_DEBUG !== "true" && level <= LogLevelType.Info + ); + this.cryptoFunctionService = new NodeCryptoFunctionService(); + this.storageService = new LowdbStorageService( + this.logService, + null, + this.dataFilePath, + false, + true + ); + this.secureStorageService = plaintextSecrets + ? this.storageService + : new KeytarSecureStorageService(applicationName); + this.cryptoService = new CryptoService( + this.storageService, + this.secureStorageService, + this.cryptoFunctionService, + this.platformUtilsService, + this.logService + ); + this.appIdService = new AppIdService(this.storageService); + this.tokenService = new TokenService(this.storageService); + this.messagingService = new NoopMessagingService(); + this.environmentService = new EnvironmentService(this.storageService); + this.apiService = new NodeApiService( + this.tokenService, + this.platformUtilsService, + this.environmentService, + () => refreshToken(this.apiKeyService, this.authService), + async (expired: boolean) => await this.logout(), + "Bitwarden_DC/" + + this.platformUtilsService.getApplicationVersion() + + " (" + + this.platformUtilsService.getDeviceString().toUpperCase() + + ")", + (clientId, clientSecret) => this.authService.logInApiKey(clientId, clientSecret) + ); + this.apiKeyService = new ApiKeyService(this.tokenService, this.storageService); + this.userService = new UserService(this.tokenService, this.storageService); + this.containerService = new ContainerService(this.cryptoService); + this.keyConnectorService = new KeyConnectorService( + this.storageService, + this.userService, + this.cryptoService, + this.apiService, + this.tokenService, + this.logService + ); + this.authService = new AuthService( + this.cryptoService, + this.apiService, + this.userService, + this.tokenService, + this.appIdService, + this.i18nService, + this.platformUtilsService, + this.messagingService, + null, + this.logService, + this.apiKeyService, + this.cryptoFunctionService, + this.environmentService, + this.keyConnectorService + ); + this.configurationService = new ConfigurationService( + this.storageService, + this.secureStorageService, + process.env.BITWARDENCLI_CONNECTOR_PLAINTEXT_SECRETS !== "true" + ); + this.syncService = new SyncService( + this.configurationService, + this.logService, + this.cryptoFunctionService, + this.apiService, + this.messagingService, + this.i18nService, + this.environmentService + ); + this.passwordGenerationService = new PasswordGenerationService( + this.cryptoService, + this.storageService, + null + ); + this.policyService = new PolicyService(this.userService, this.storageService, this.apiService); + this.settingsService = new SettingsService(this.userService, this.storageService); + this.fileUploadService = new FileUploadService(this.logService, this.apiService); + this.cipherService = new CipherService( + this.cryptoService, + this.userService, + this.settingsService, + this.apiService, + this.fileUploadService, + this.storageService, + this.i18nService, + () => searchService, + this.logService + ); + this.searchService = new SearchService(this.cipherService, this.logService, this.i18nService); + this.folderService = new FolderService( + this.cryptoService, + this.userService, + this.apiService, + this.storageService, + this.i18nService, + this.cipherService + ); + this.collectionService = new CollectionService( + this.cryptoService, + this.userService, + this.storageService, + this.i18nService + ); + this.sendService = new SendService( + this.cryptoService, + this.userService, + this.apiService, + this.fileUploadService, + this.storageService, + this.i18nService, + this.cryptoFunctionService + ); - async logout() { - await this.tokenService.clearToken(); - await this.apiKeyService.clear(); - } + this.loginSyncService = new LoginSyncService( + this.userService, + this.apiService, + this.settingsService, + this.folderService, + this.cipherService, + this.cryptoService, + this.collectionService, + this.storageService, + this.messagingService, + this.policyService, + this.sendService, + this.logService, + this.tokenService, + this.keyConnectorService, + async (expired: boolean) => this.messagingService.send("logout", { expired: expired }) + ); - private async init() { - await this.storageService.init(); - this.containerService.attachToWindow(global); - await this.environmentService.setUrlsFromStorage(); - // Dev Server URLs. Comment out the line above. - // this.apiService.setUrls({ - // base: null, - // api: 'http://localhost:4000', - // identity: 'http://localhost:33656', - // }); - const locale = await this.storageService.get(ConstantsService.localeKey); - await this.i18nService.init(locale); - this.authService.init(); + this.program = new Program(this); + } - const installedVersion = await this.storageService.get(ConstantsService.installedVersionKey); - const currentVersion = await this.platformUtilsService.getApplicationVersion(); - if (installedVersion == null || installedVersion !== currentVersion) { - await this.storageService.save(ConstantsService.installedVersionKey, currentVersion); - } + async run() { + await this.init(); + this.program.run(); + } + + async logout() { + await this.tokenService.clearToken(); + await this.apiKeyService.clear(); + } + + private async init() { + await this.storageService.init(); + this.containerService.attachToWindow(global); + await this.environmentService.setUrlsFromStorage(); + // Dev Server URLs. Comment out the line above. + // this.apiService.setUrls({ + // base: null, + // api: 'http://localhost:4000', + // identity: 'http://localhost:33656', + // }); + const locale = await this.storageService.get(ConstantsService.localeKey); + await this.i18nService.init(locale); + this.authService.init(); + + const installedVersion = await this.storageService.get( + ConstantsService.installedVersionKey + ); + const currentVersion = await this.platformUtilsService.getApplicationVersion(); + if (installedVersion == null || installedVersion !== currentVersion) { + await this.storageService.save(ConstantsService.installedVersionKey, currentVersion); } + } } const main = new Main(); diff --git a/src/commands/clearCache.command.ts b/src/commands/clearCache.command.ts index 946117a7..5831baaf 100644 --- a/src/commands/clearCache.command.ts +++ b/src/commands/clearCache.command.ts @@ -1,22 +1,25 @@ -import * as program from 'commander'; +import * as program from "commander"; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; +import { I18nService } from "jslib-common/abstractions/i18n.service"; -import { ConfigurationService } from '../services/configuration.service'; +import { ConfigurationService } from "../services/configuration.service"; -import { Response } from 'jslib-node/cli/models/response'; -import { MessageResponse } from 'jslib-node/cli/models/response/messageResponse'; +import { Response } from "jslib-node/cli/models/response"; +import { MessageResponse } from "jslib-node/cli/models/response/messageResponse"; export class ClearCacheCommand { - constructor(private configurationService: ConfigurationService, private i18nService: I18nService) { } + constructor( + private configurationService: ConfigurationService, + private i18nService: I18nService + ) {} - async run(cmd: program.OptionValues): Promise { - try { - await this.configurationService.clearStatefulSettings(true); - const res = new MessageResponse(this.i18nService.t('syncCacheCleared'), null); - return Response.success(res); - } catch (e) { - return Response.error(e); - } + async run(cmd: program.OptionValues): Promise { + try { + await this.configurationService.clearStatefulSettings(true); + const res = new MessageResponse(this.i18nService.t("syncCacheCleared"), null); + return Response.success(res); + } catch (e) { + return Response.error(e); } + } } diff --git a/src/commands/config.command.ts b/src/commands/config.command.ts index b314020f..d50f4e29 100644 --- a/src/commands/config.command.ts +++ b/src/commands/config.command.ts @@ -1,150 +1,160 @@ -import * as program from 'commander'; +import * as program from "commander"; -import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; +import { EnvironmentService } from "jslib-common/abstractions/environment.service"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; -import { ConfigurationService } from '../services/configuration.service'; +import { ConfigurationService } from "../services/configuration.service"; -import { DirectoryType } from '../enums/directoryType'; +import { DirectoryType } from "../enums/directoryType"; -import { Response } from 'jslib-node/cli/models/response'; -import { MessageResponse } from 'jslib-node/cli/models/response/messageResponse'; +import { Response } from "jslib-node/cli/models/response"; +import { MessageResponse } from "jslib-node/cli/models/response/messageResponse"; -import { AzureConfiguration } from '../models/azureConfiguration'; -import { GSuiteConfiguration } from '../models/gsuiteConfiguration'; -import { LdapConfiguration } from '../models/ldapConfiguration'; -import { OktaConfiguration } from '../models/oktaConfiguration'; -import { OneLoginConfiguration } from '../models/oneLoginConfiguration'; -import { SyncConfiguration } from '../models/syncConfiguration'; +import { AzureConfiguration } from "../models/azureConfiguration"; +import { GSuiteConfiguration } from "../models/gsuiteConfiguration"; +import { LdapConfiguration } from "../models/ldapConfiguration"; +import { OktaConfiguration } from "../models/oktaConfiguration"; +import { OneLoginConfiguration } from "../models/oneLoginConfiguration"; +import { SyncConfiguration } from "../models/syncConfiguration"; -import { ConnectorUtils } from '../utils'; +import { ConnectorUtils } from "../utils"; -import { NodeUtils } from 'jslib-common/misc/nodeUtils'; +import { NodeUtils } from "jslib-common/misc/nodeUtils"; export class ConfigCommand { - private directory: DirectoryType; - private ldap = new LdapConfiguration(); - private gsuite = new GSuiteConfiguration(); - private azure = new AzureConfiguration(); - private okta = new OktaConfiguration(); - private oneLogin = new OneLoginConfiguration(); - private sync = new SyncConfiguration(); + private directory: DirectoryType; + private ldap = new LdapConfiguration(); + private gsuite = new GSuiteConfiguration(); + private azure = new AzureConfiguration(); + private okta = new OktaConfiguration(); + private oneLogin = new OneLoginConfiguration(); + private sync = new SyncConfiguration(); - constructor(private environmentService: EnvironmentService, private i18nService: I18nService, - private configurationService: ConfigurationService) { } + constructor( + private environmentService: EnvironmentService, + private i18nService: I18nService, + private configurationService: ConfigurationService + ) {} - async run(setting: string, value: string, options: program.OptionValues): Promise { - setting = setting.toLowerCase(); - if (value == null || value === '') { - if (options.secretfile) { - value = await NodeUtils.readFirstLine(options.secretfile); - } else if (options.secretenv && process.env[options.secretenv]) { - value = process.env[options.secretenv]; - } - } - try { - switch (setting) { - case 'server': - await this.setServer(value); - break; - case 'directory': - await this.setDirectory(value); - break; - case 'ldap.password': - await this.setLdapPassword(value); - break; - case 'gsuite.key': - await this.setGSuiteKey(value); - break; - case 'azure.key': - await this.setAzureKey(value); - break; - case 'okta.token': - await this.setOktaToken(value); - break; - case 'onelogin.secret': - await this.setOneLoginSecret(value); - break; - default: - return Response.badRequest('Unknown setting.'); - } - } catch (e) { - return Response.error(e); - } - const res = new MessageResponse(this.i18nService.t('savedSetting', setting), null); - return Response.success(res); + async run(setting: string, value: string, options: program.OptionValues): Promise { + setting = setting.toLowerCase(); + if (value == null || value === "") { + if (options.secretfile) { + value = await NodeUtils.readFirstLine(options.secretfile); + } else if (options.secretenv && process.env[options.secretenv]) { + value = process.env[options.secretenv]; + } } - - private async setServer(url: string) { - url = (url === 'null' || url === 'bitwarden.com' || url === 'https://bitwarden.com' ? null : url); - await this.environmentService.setUrls({ - base: url, - }); + try { + switch (setting) { + case "server": + await this.setServer(value); + break; + case "directory": + await this.setDirectory(value); + break; + case "ldap.password": + await this.setLdapPassword(value); + break; + case "gsuite.key": + await this.setGSuiteKey(value); + break; + case "azure.key": + await this.setAzureKey(value); + break; + case "okta.token": + await this.setOktaToken(value); + break; + case "onelogin.secret": + await this.setOneLoginSecret(value); + break; + default: + return Response.badRequest("Unknown setting."); + } + } catch (e) { + return Response.error(e); } + const res = new MessageResponse(this.i18nService.t("savedSetting", setting), null); + return Response.success(res); + } - private async setDirectory(type: string) { - const dir = parseInt(type, null); - if (dir < DirectoryType.Ldap || dir > DirectoryType.OneLogin) { - throw new Error('Invalid directory type value.'); - } - await this.loadConfig(); - this.directory = dir; - await this.saveConfig(); - } + private async setServer(url: string) { + url = url === "null" || url === "bitwarden.com" || url === "https://bitwarden.com" ? null : url; + await this.environmentService.setUrls({ + base: url, + }); + } - private async setLdapPassword(password: string) { - await this.loadConfig(); - this.ldap.password = password; - await this.saveConfig(); + private async setDirectory(type: string) { + const dir = parseInt(type, null); + if (dir < DirectoryType.Ldap || dir > DirectoryType.OneLogin) { + throw new Error("Invalid directory type value."); } + await this.loadConfig(); + this.directory = dir; + await this.saveConfig(); + } - private async setGSuiteKey(key: string) { - await this.loadConfig(); - this.gsuite.privateKey = key != null ? key.trimLeft() : null; - await this.saveConfig(); - } + private async setLdapPassword(password: string) { + await this.loadConfig(); + this.ldap.password = password; + await this.saveConfig(); + } - private async setAzureKey(key: string) { - await this.loadConfig(); - this.azure.key = key; - await this.saveConfig(); - } + private async setGSuiteKey(key: string) { + await this.loadConfig(); + this.gsuite.privateKey = key != null ? key.trimLeft() : null; + await this.saveConfig(); + } - private async setOktaToken(token: string) { - await this.loadConfig(); - this.okta.token = token; - await this.saveConfig(); - } + private async setAzureKey(key: string) { + await this.loadConfig(); + this.azure.key = key; + await this.saveConfig(); + } - private async setOneLoginSecret(secret: string) { - await this.loadConfig(); - this.oneLogin.clientSecret = secret; - await this.saveConfig(); - } + private async setOktaToken(token: string) { + await this.loadConfig(); + this.okta.token = token; + await this.saveConfig(); + } - private async loadConfig() { - this.directory = await this.configurationService.getDirectoryType(); - this.ldap = (await this.configurationService.getDirectory(DirectoryType.Ldap)) || - this.ldap; - this.gsuite = (await this.configurationService.getDirectory(DirectoryType.GSuite)) || - this.gsuite; - this.azure = (await this.configurationService.getDirectory( - DirectoryType.AzureActiveDirectory)) || this.azure; - this.okta = (await this.configurationService.getDirectory( - DirectoryType.Okta)) || this.okta; - this.oneLogin = (await this.configurationService.getDirectory( - DirectoryType.OneLogin)) || this.oneLogin; - this.sync = (await this.configurationService.getSync()) || this.sync; - } + private async setOneLoginSecret(secret: string) { + await this.loadConfig(); + this.oneLogin.clientSecret = secret; + await this.saveConfig(); + } - private async saveConfig() { - ConnectorUtils.adjustConfigForSave(this.ldap, this.sync); - await this.configurationService.saveDirectoryType(this.directory); - await this.configurationService.saveDirectory(DirectoryType.Ldap, this.ldap); - await this.configurationService.saveDirectory(DirectoryType.GSuite, this.gsuite); - await this.configurationService.saveDirectory(DirectoryType.AzureActiveDirectory, this.azure); - await this.configurationService.saveDirectory(DirectoryType.Okta, this.okta); - await this.configurationService.saveDirectory(DirectoryType.OneLogin, this.oneLogin); - await this.configurationService.saveSync(this.sync); - } + private async loadConfig() { + this.directory = await this.configurationService.getDirectoryType(); + this.ldap = + (await this.configurationService.getDirectory(DirectoryType.Ldap)) || + this.ldap; + this.gsuite = + (await this.configurationService.getDirectory(DirectoryType.GSuite)) || + this.gsuite; + this.azure = + (await this.configurationService.getDirectory( + DirectoryType.AzureActiveDirectory + )) || this.azure; + this.okta = + (await this.configurationService.getDirectory(DirectoryType.Okta)) || + this.okta; + this.oneLogin = + (await this.configurationService.getDirectory( + DirectoryType.OneLogin + )) || this.oneLogin; + this.sync = (await this.configurationService.getSync()) || this.sync; + } + + private async saveConfig() { + ConnectorUtils.adjustConfigForSave(this.ldap, this.sync); + await this.configurationService.saveDirectoryType(this.directory); + await this.configurationService.saveDirectory(DirectoryType.Ldap, this.ldap); + await this.configurationService.saveDirectory(DirectoryType.GSuite, this.gsuite); + await this.configurationService.saveDirectory(DirectoryType.AzureActiveDirectory, this.azure); + await this.configurationService.saveDirectory(DirectoryType.Okta, this.okta); + await this.configurationService.saveDirectory(DirectoryType.OneLogin, this.oneLogin); + await this.configurationService.saveSync(this.sync); + } } diff --git a/src/commands/lastSync.command.ts b/src/commands/lastSync.command.ts index eacc88e5..ccccce5f 100644 --- a/src/commands/lastSync.command.ts +++ b/src/commands/lastSync.command.ts @@ -1,29 +1,31 @@ -import * as program from 'commander'; +import * as program from "commander"; -import { ConfigurationService } from '../services/configuration.service'; +import { ConfigurationService } from "../services/configuration.service"; -import { Response } from 'jslib-node/cli/models/response'; -import { StringResponse } from 'jslib-node/cli/models/response/stringResponse'; +import { Response } from "jslib-node/cli/models/response"; +import { StringResponse } from "jslib-node/cli/models/response/stringResponse"; export class LastSyncCommand { - constructor(private configurationService: ConfigurationService) { } + constructor(private configurationService: ConfigurationService) {} - async run(object: string): Promise { - 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); - } + async run(object: string): Promise { + 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); } + } } diff --git a/src/commands/sync.command.ts b/src/commands/sync.command.ts index c9f4300b..89c00111 100644 --- a/src/commands/sync.command.ts +++ b/src/commands/sync.command.ts @@ -1,23 +1,25 @@ -import { I18nService } from 'jslib-common/abstractions/i18n.service'; +import { I18nService } from "jslib-common/abstractions/i18n.service"; -import { SyncService } from '../services/sync.service'; +import { SyncService } from "../services/sync.service"; -import { Response } from 'jslib-node/cli/models/response'; -import { MessageResponse } from 'jslib-node/cli/models/response/messageResponse'; +import { Response } from "jslib-node/cli/models/response"; +import { MessageResponse } from "jslib-node/cli/models/response/messageResponse"; export class SyncCommand { - constructor(private syncService: SyncService, private i18nService: I18nService) { } + constructor(private syncService: SyncService, private i18nService: I18nService) {} - async run(): Promise { - try { - const result = await this.syncService.sync(false, false); - const groupCount = result[0] != null ? result[0].length : 0; - const userCount = result[1] != null ? result[1].length : 0; - const res = new MessageResponse(this.i18nService.t('syncingComplete'), - this.i18nService.t('syncCounts', groupCount.toString(), userCount.toString())); - return Response.success(res); - } catch (e) { - return Response.error(e); - } + async run(): Promise { + try { + const result = await this.syncService.sync(false, false); + const groupCount = result[0] != null ? result[0].length : 0; + const userCount = result[1] != null ? result[1].length : 0; + const res = new MessageResponse( + this.i18nService.t("syncingComplete"), + this.i18nService.t("syncCounts", groupCount.toString(), userCount.toString()) + ); + return Response.success(res); + } catch (e) { + return Response.error(e); } + } } diff --git a/src/commands/test.command.ts b/src/commands/test.command.ts index c5122f29..34addb95 100644 --- a/src/commands/test.command.ts +++ b/src/commands/test.command.ts @@ -1,24 +1,28 @@ -import * as program from 'commander'; +import * as program from "commander"; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; +import { I18nService } from "jslib-common/abstractions/i18n.service"; -import { SyncService } from '../services/sync.service'; +import { SyncService } from "../services/sync.service"; -import { ConnectorUtils } from '../utils'; +import { ConnectorUtils } from "../utils"; -import { Response } from 'jslib-node/cli/models/response'; -import { TestResponse } from '../models/response/testResponse'; +import { Response } from "jslib-node/cli/models/response"; +import { TestResponse } from "../models/response/testResponse"; export class TestCommand { - constructor(private syncService: SyncService, private i18nService: I18nService) { } + constructor(private syncService: SyncService, private i18nService: I18nService) {} - async run(cmd: program.OptionValues): Promise { - try { - const result = await ConnectorUtils.simulate(this.syncService, this.i18nService, cmd.last || false); - const res = new TestResponse(result); - return Response.success(res); - } catch (e) { - return Response.error(e); - } + async run(cmd: program.OptionValues): Promise { + try { + const result = await ConnectorUtils.simulate( + this.syncService, + this.i18nService, + cmd.last || false + ); + const res = new TestResponse(result); + return Response.success(res); + } catch (e) { + return Response.error(e); } + } } diff --git a/src/enums/directoryType.ts b/src/enums/directoryType.ts index 4324fc06..cb3ad986 100644 --- a/src/enums/directoryType.ts +++ b/src/enums/directoryType.ts @@ -1,7 +1,7 @@ export enum DirectoryType { - Ldap = 0, - AzureActiveDirectory = 1, - GSuite = 2, - Okta = 3, - OneLogin = 4, + Ldap = 0, + AzureActiveDirectory = 1, + GSuite = 2, + Okta = 3, + OneLogin = 4, } diff --git a/src/global.d.ts b/src/global.d.ts index 8116ab17..74e63a31 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -1,3 +1,3 @@ declare function escape(s: string): string; declare function unescape(s: string): string; -declare module 'duo_web_sdk'; +declare module "duo_web_sdk"; diff --git a/src/index.html b/src/index.html index 9c16f5d7..2ec2d77c 100644 --- a/src/index.html +++ b/src/index.html @@ -1,16 +1,19 @@  - - - - + + + + Bitwarden Directory Connector - - - + + + -
+
- + diff --git a/src/main.ts b/src/main.ts index e9f89f8f..33180008 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,109 +1,135 @@ -import { app } from 'electron'; -import * as path from 'path'; +import { app } from "electron"; +import * as path from "path"; -import { MenuMain } from './main/menu.main'; -import { MessagingMain } from './main/messaging.main'; -import { I18nService } from './services/i18n.service'; +import { MenuMain } from "./main/menu.main"; +import { MessagingMain } from "./main/messaging.main"; +import { I18nService } from "./services/i18n.service"; -import { KeytarStorageListener } from 'jslib-electron/keytarStorageListener'; -import { ElectronLogService } from 'jslib-electron/services/electronLog.service'; -import { ElectronMainMessagingService } from 'jslib-electron/services/electronMainMessaging.service'; -import { ElectronStorageService } from 'jslib-electron/services/electronStorage.service'; -import { TrayMain } from 'jslib-electron/tray.main'; -import { UpdaterMain } from 'jslib-electron/updater.main'; -import { WindowMain } from 'jslib-electron/window.main'; +import { KeytarStorageListener } from "jslib-electron/keytarStorageListener"; +import { ElectronLogService } from "jslib-electron/services/electronLog.service"; +import { ElectronMainMessagingService } from "jslib-electron/services/electronMainMessaging.service"; +import { ElectronStorageService } from "jslib-electron/services/electronStorage.service"; +import { TrayMain } from "jslib-electron/tray.main"; +import { UpdaterMain } from "jslib-electron/updater.main"; +import { WindowMain } from "jslib-electron/window.main"; export class Main { - logService: ElectronLogService; - i18nService: I18nService; - storageService: ElectronStorageService; - messagingService: ElectronMainMessagingService; - keytarStorageListener: KeytarStorageListener; + logService: ElectronLogService; + i18nService: I18nService; + storageService: ElectronStorageService; + messagingService: ElectronMainMessagingService; + keytarStorageListener: KeytarStorageListener; - windowMain: WindowMain; - messagingMain: MessagingMain; - menuMain: MenuMain; - updaterMain: UpdaterMain; - trayMain: TrayMain; + windowMain: WindowMain; + messagingMain: MessagingMain; + menuMain: MenuMain; + updaterMain: UpdaterMain; + trayMain: TrayMain; - constructor() { - // Set paths for portable builds - let appDataPath = null; - if (process.env.BITWARDEN_CONNECTOR_APPDATA_DIR != null) { - appDataPath = process.env.BITWARDEN_CONNECTOR_APPDATA_DIR; - } else if (process.platform === 'win32' && process.env.PORTABLE_EXECUTABLE_DIR != null) { - appDataPath = path.join(process.env.PORTABLE_EXECUTABLE_DIR, 'bitwarden-connector-appdata'); - } - - if (appDataPath != null) { - app.setPath('userData', appDataPath); - } - app.setPath('logs', path.join(app.getPath('userData'), 'logs')); - - const args = process.argv.slice(1); - const watch = args.some(val => val === '--watch'); - - if (watch) { - // tslint:disable-next-line - require('electron-reload')(__dirname, {}); - } - - this.logService = new ElectronLogService(null, app.getPath('userData')); - this.i18nService = new I18nService('en', './locales/'); - this.storageService = new ElectronStorageService(app.getPath('userData')); - - this.windowMain = new WindowMain(this.storageService, this.logService, false, 800, 600, arg => this.processDeepLink(arg), null); - this.menuMain = new MenuMain(this); - this.updaterMain = new UpdaterMain(this.i18nService, this.windowMain, 'directory-connector', () => { - this.messagingService.send('checkingForUpdate'); - }, () => { - this.messagingService.send('doneCheckingForUpdate'); - }, () => { - this.messagingService.send('doneCheckingForUpdate'); - }, 'bitwardenDirectoryConnector'); - this.trayMain = new TrayMain(this.windowMain, this.i18nService, this.storageService); - this.messagingMain = new MessagingMain(this.windowMain, this.menuMain, this.updaterMain, this.trayMain); - this.messagingService = new ElectronMainMessagingService(this.windowMain, message => { - this.messagingMain.onMessage(message); - }); - - this.keytarStorageListener = new KeytarStorageListener('Bitwarden Directory Connector', null); + constructor() { + // Set paths for portable builds + let appDataPath = null; + if (process.env.BITWARDEN_CONNECTOR_APPDATA_DIR != null) { + appDataPath = process.env.BITWARDEN_CONNECTOR_APPDATA_DIR; + } else if (process.platform === "win32" && process.env.PORTABLE_EXECUTABLE_DIR != null) { + appDataPath = path.join(process.env.PORTABLE_EXECUTABLE_DIR, "bitwarden-connector-appdata"); } - bootstrap() { - this.keytarStorageListener.init(); - this.windowMain.init().then(async () => { - await this.i18nService.init(app.getLocale()); - this.menuMain.init(); - this.messagingMain.init(); - await this.updaterMain.init(); - await this.trayMain.init(this.i18nService.t('bitwardenDirectoryConnector')); + if (appDataPath != null) { + app.setPath("userData", appDataPath); + } + app.setPath("logs", path.join(app.getPath("userData"), "logs")); - if (!app.isDefaultProtocolClient('bwdc')) { - app.setAsDefaultProtocolClient('bwdc'); - } + const args = process.argv.slice(1); + const watch = args.some((val) => val === "--watch"); - // Process protocol for macOS - app.on('open-url', (event, url) => { - event.preventDefault(); - this.processDeepLink([url]); - }); - }, (e: any) => { - // tslint:disable-next-line - console.error(e); - }); + if (watch) { + // tslint:disable-next-line + require("electron-reload")(__dirname, {}); } - private processDeepLink(argv: string[]): void { - argv.filter(s => s.indexOf('bwdc://') === 0).forEach(s => { - const url = new URL(s); - const code = url.searchParams.get('code'); - const receivedState = url.searchParams.get('state'); - if (code != null && receivedState != null) { - this.messagingService.send('ssoCallback', { code: code, state: receivedState }); - } + this.logService = new ElectronLogService(null, app.getPath("userData")); + this.i18nService = new I18nService("en", "./locales/"); + this.storageService = new ElectronStorageService(app.getPath("userData")); + + this.windowMain = new WindowMain( + this.storageService, + this.logService, + false, + 800, + 600, + (arg) => this.processDeepLink(arg), + null + ); + this.menuMain = new MenuMain(this); + this.updaterMain = new UpdaterMain( + this.i18nService, + this.windowMain, + "directory-connector", + () => { + this.messagingService.send("checkingForUpdate"); + }, + () => { + this.messagingService.send("doneCheckingForUpdate"); + }, + () => { + this.messagingService.send("doneCheckingForUpdate"); + }, + "bitwardenDirectoryConnector" + ); + this.trayMain = new TrayMain(this.windowMain, this.i18nService, this.storageService); + this.messagingMain = new MessagingMain( + this.windowMain, + this.menuMain, + this.updaterMain, + this.trayMain + ); + this.messagingService = new ElectronMainMessagingService(this.windowMain, (message) => { + this.messagingMain.onMessage(message); + }); + + this.keytarStorageListener = new KeytarStorageListener("Bitwarden Directory Connector", null); + } + + bootstrap() { + this.keytarStorageListener.init(); + this.windowMain.init().then( + async () => { + await this.i18nService.init(app.getLocale()); + this.menuMain.init(); + this.messagingMain.init(); + await this.updaterMain.init(); + await this.trayMain.init(this.i18nService.t("bitwardenDirectoryConnector")); + + if (!app.isDefaultProtocolClient("bwdc")) { + app.setAsDefaultProtocolClient("bwdc"); + } + + // Process protocol for macOS + app.on("open-url", (event, url) => { + event.preventDefault(); + this.processDeepLink([url]); }); - } + }, + (e: any) => { + // tslint:disable-next-line + console.error(e); + } + ); + } + + private processDeepLink(argv: string[]): void { + argv + .filter((s) => s.indexOf("bwdc://") === 0) + .forEach((s) => { + const url = new URL(s); + const code = url.searchParams.get("code"); + const receivedState = url.searchParams.get("state"); + if (code != null && receivedState != null) { + this.messagingService.send("ssoCallback", { code: code, state: receivedState }); + } + }); + } } const main = new Main(); diff --git a/src/main/menu.main.ts b/src/main/menu.main.ts index 7f2741c2..99467d62 100644 --- a/src/main/menu.main.ts +++ b/src/main/menu.main.ts @@ -1,68 +1,69 @@ -import { - Menu, - MenuItem, - MenuItemConstructorOptions, -} from 'electron'; +import { Menu, MenuItem, MenuItemConstructorOptions } from "electron"; -import { Main } from '../main'; +import { Main } from "../main"; -import { BaseMenu } from 'jslib-electron/baseMenu'; +import { BaseMenu } from "jslib-electron/baseMenu"; export class MenuMain extends BaseMenu { - menu: Menu; + menu: Menu; - constructor(private main: Main) { - super(main.i18nService, main.windowMain); + constructor(private main: Main) { + super(main.i18nService, main.windowMain); + } + + init() { + this.initProperties(); + this.initContextMenu(); + this.initApplicationMenu(); + } + + private initApplicationMenu() { + const template: MenuItemConstructorOptions[] = [ + this.editMenuItemOptions, + { + label: this.i18nService.t("view"), + submenu: this.viewSubMenuItemOptions, + }, + this.windowMenuItemOptions, + ]; + + if (process.platform === "darwin") { + const firstMenuPart: MenuItemConstructorOptions[] = [ + { + label: this.i18nService.t("aboutBitwarden"), + role: "about", + }, + ]; + + template.unshift({ + label: this.main.i18nService.t("bitwardenDirectoryConnector"), + submenu: firstMenuPart.concat(this.macAppMenuItemOptions), + }); + + // Window menu + template[template.length - 1].submenu = this.macWindowSubmenuOptions; } - init() { - this.initProperties(); - this.initContextMenu(); - this.initApplicationMenu(); - } + (template[template.length - 1].submenu as MenuItemConstructorOptions[]).splice( + 1, + 0, + { + label: this.main.i18nService.t( + process.platform === "darwin" ? "hideToMenuBar" : "hideToTray" + ), + click: () => this.main.messagingService.send("hideToTray"), + accelerator: "CmdOrCtrl+Shift+M", + }, + { + type: "checkbox", + label: this.main.i18nService.t("alwaysOnTop"), + checked: this.windowMain.win.isAlwaysOnTop(), + click: () => this.main.windowMain.toggleAlwaysOnTop(), + accelerator: "CmdOrCtrl+Shift+T", + } + ); - private initApplicationMenu() { - const template: MenuItemConstructorOptions[] = [ - this.editMenuItemOptions, - { - label: this.i18nService.t('view'), - submenu: this.viewSubMenuItemOptions, - }, - this.windowMenuItemOptions, - ]; - - if (process.platform === 'darwin') { - const firstMenuPart: MenuItemConstructorOptions[] = [ - { - label: this.i18nService.t('aboutBitwarden'), - role: 'about', - }, - ]; - - template.unshift({ - label: this.main.i18nService.t('bitwardenDirectoryConnector'), - submenu: firstMenuPart.concat(this.macAppMenuItemOptions), - }); - - // Window menu - template[template.length - 1].submenu = this.macWindowSubmenuOptions; - } - - (template[template.length - 1].submenu as MenuItemConstructorOptions[]).splice(1, 0, - { - label: this.main.i18nService.t(process.platform === 'darwin' ? 'hideToMenuBar' : 'hideToTray'), - click: () => this.main.messagingService.send('hideToTray'), - accelerator: 'CmdOrCtrl+Shift+M', - }, - { - type: 'checkbox', - label: this.main.i18nService.t('alwaysOnTop'), - checked: this.windowMain.win.isAlwaysOnTop(), - click: () => this.main.windowMain.toggleAlwaysOnTop(), - accelerator: 'CmdOrCtrl+Shift+T', - }); - - this.menu = Menu.buildFromTemplate(template); - Menu.setApplicationMenu(this.menu); - } + this.menu = Menu.buildFromTemplate(template); + Menu.setApplicationMenu(this.menu); + } } diff --git a/src/main/messaging.main.ts b/src/main/messaging.main.ts index 4658445f..8bc70500 100644 --- a/src/main/messaging.main.ts +++ b/src/main/messaging.main.ts @@ -1,67 +1,68 @@ -import { - app, - ipcMain, -} from 'electron'; +import { app, ipcMain } from "electron"; -import { TrayMain } from 'jslib-electron/tray.main'; -import { UpdaterMain } from 'jslib-electron/updater.main'; -import { WindowMain } from 'jslib-electron/window.main'; +import { TrayMain } from "jslib-electron/tray.main"; +import { UpdaterMain } from "jslib-electron/updater.main"; +import { WindowMain } from "jslib-electron/window.main"; -import { MenuMain } from './menu.main'; +import { MenuMain } from "./menu.main"; const SyncCheckInterval = 60 * 1000; // 1 minute export class MessagingMain { - private syncTimeout: NodeJS.Timer; + private syncTimeout: NodeJS.Timer; - constructor(private windowMain: WindowMain, private menuMain: MenuMain, - private updaterMain: UpdaterMain, private trayMain: TrayMain) { } + constructor( + private windowMain: WindowMain, + private menuMain: MenuMain, + private updaterMain: UpdaterMain, + private trayMain: TrayMain + ) {} - init() { - ipcMain.on('messagingService', async (event: any, message: any) => this.onMessage(message)); - } + init() { + ipcMain.on("messagingService", async (event: any, message: any) => this.onMessage(message)); + } - onMessage(message: any) { - switch (message.command) { - case 'checkForUpdate': - this.updaterMain.checkForUpdate(true); - break; - case 'scheduleNextDirSync': - this.scheduleNextSync(); - break; - case 'cancelDirSync': - this.windowMain.win.webContents.send('messagingService', { - command: 'syncScheduleStopped', - }); - if (this.syncTimeout) { - global.clearTimeout(this.syncTimeout); - } - break; - case 'hideToTray': - this.trayMain.hideToTray(); - break; - default: - break; - } - } - - private scheduleNextSync() { - this.windowMain.win.webContents.send('messagingService', { - command: 'syncScheduleStarted', + onMessage(message: any) { + switch (message.command) { + case "checkForUpdate": + this.updaterMain.checkForUpdate(true); + break; + case "scheduleNextDirSync": + this.scheduleNextSync(); + break; + case "cancelDirSync": + this.windowMain.win.webContents.send("messagingService", { + command: "syncScheduleStopped", }); - if (this.syncTimeout) { - global.clearTimeout(this.syncTimeout); + global.clearTimeout(this.syncTimeout); } - - this.syncTimeout = global.setTimeout(() => { - if (this.windowMain.win == null) { - return; - } - - this.windowMain.win.webContents.send('messagingService', { - command: 'checkDirSync', - }); - }, SyncCheckInterval); + break; + case "hideToTray": + this.trayMain.hideToTray(); + break; + default: + break; } + } + + private scheduleNextSync() { + this.windowMain.win.webContents.send("messagingService", { + command: "syncScheduleStarted", + }); + + if (this.syncTimeout) { + global.clearTimeout(this.syncTimeout); + } + + this.syncTimeout = global.setTimeout(() => { + if (this.windowMain.win == null) { + return; + } + + this.windowMain.win.webContents.send("messagingService", { + command: "checkDirSync", + }); + }, SyncCheckInterval); + } } diff --git a/src/models/azureConfiguration.ts b/src/models/azureConfiguration.ts index 904aec8c..38c61338 100644 --- a/src/models/azureConfiguration.ts +++ b/src/models/azureConfiguration.ts @@ -1,6 +1,6 @@ export class AzureConfiguration { - identityAuthority: string; - tenant: string; - applicationId: string; - key: string; + identityAuthority: string; + tenant: string; + applicationId: string; + key: string; } diff --git a/src/models/entry.ts b/src/models/entry.ts index 4bfe120a..1cf3e383 100644 --- a/src/models/entry.ts +++ b/src/models/entry.ts @@ -1,8 +1,8 @@ export abstract class Entry { - referenceId: string; - externalId: string; + referenceId: string; + externalId: string; - get displayName(): string { - return this.referenceId; - } + get displayName(): string { + return this.referenceId; + } } diff --git a/src/models/groupEntry.ts b/src/models/groupEntry.ts index 207699a6..8d2c9d22 100644 --- a/src/models/groupEntry.ts +++ b/src/models/groupEntry.ts @@ -1,15 +1,15 @@ -import { Entry } from './entry'; +import { Entry } from "./entry"; export class GroupEntry extends Entry { - name: string; - userMemberExternalIds = new Set(); - groupMemberReferenceIds = new Set(); + name: string; + userMemberExternalIds = new Set(); + groupMemberReferenceIds = new Set(); - get displayName(): string { - if (this.name == null) { - return this.referenceId; - } - - return this.name; + get displayName(): string { + if (this.name == null) { + return this.referenceId; } + + return this.name; + } } diff --git a/src/models/gsuiteConfiguration.ts b/src/models/gsuiteConfiguration.ts index cbe18e8b..6d147993 100644 --- a/src/models/gsuiteConfiguration.ts +++ b/src/models/gsuiteConfiguration.ts @@ -1,7 +1,7 @@ export class GSuiteConfiguration { - clientEmail: string; - privateKey: string; - domain: string; - adminUser: string; - customer: string; + clientEmail: string; + privateKey: string; + domain: string; + adminUser: string; + customer: string; } diff --git a/src/models/ldapConfiguration.ts b/src/models/ldapConfiguration.ts index dd1f7556..3a1cbf40 100644 --- a/src/models/ldapConfiguration.ts +++ b/src/models/ldapConfiguration.ts @@ -1,18 +1,18 @@ export class LdapConfiguration { - ssl = false; - startTls = false; - tlsCaPath: string; - sslAllowUnauthorized = false; - sslCertPath: string; - sslKeyPath: string; - sslCaPath: string; - hostname: string; - port = 389; - domain: string; - rootPath: string; - currentUser = false; - username: string; - password: string; - ad = true; - pagedSearch = true; + ssl = false; + startTls = false; + tlsCaPath: string; + sslAllowUnauthorized = false; + sslCertPath: string; + sslKeyPath: string; + sslCaPath: string; + hostname: string; + port = 389; + domain: string; + rootPath: string; + currentUser = false; + username: string; + password: string; + ad = true; + pagedSearch = true; } diff --git a/src/models/oktaConfiguration.ts b/src/models/oktaConfiguration.ts index 3e983026..88f77f41 100644 --- a/src/models/oktaConfiguration.ts +++ b/src/models/oktaConfiguration.ts @@ -1,4 +1,4 @@ export class OktaConfiguration { - orgUrl: string; - token: string; + orgUrl: string; + token: string; } diff --git a/src/models/oneLoginConfiguration.ts b/src/models/oneLoginConfiguration.ts index b9a2402f..76d0f13a 100644 --- a/src/models/oneLoginConfiguration.ts +++ b/src/models/oneLoginConfiguration.ts @@ -1,5 +1,5 @@ export class OneLoginConfiguration { - clientId: string; - clientSecret: string; - region = 'us'; + clientId: string; + clientSecret: string; + region = "us"; } diff --git a/src/models/response/groupResponse.ts b/src/models/response/groupResponse.ts index d0f9c4e3..76348b42 100644 --- a/src/models/response/groupResponse.ts +++ b/src/models/response/groupResponse.ts @@ -1,13 +1,13 @@ -import { GroupEntry } from '../groupEntry'; +import { GroupEntry } from "../groupEntry"; export class GroupResponse { - externalId: string; - displayName: string; - userIds: string[]; + externalId: string; + displayName: string; + userIds: string[]; - constructor(g: GroupEntry) { - this.externalId = g.externalId; - this.displayName = g.displayName; - this.userIds = Array.from(g.userMemberExternalIds); - } + constructor(g: GroupEntry) { + this.externalId = g.externalId; + this.displayName = g.displayName; + this.userIds = Array.from(g.userMemberExternalIds); + } } diff --git a/src/models/response/testResponse.ts b/src/models/response/testResponse.ts index b16f4294..1d14ca5a 100644 --- a/src/models/response/testResponse.ts +++ b/src/models/response/testResponse.ts @@ -1,22 +1,25 @@ -import { GroupResponse } from './groupResponse'; -import { UserResponse } from './userResponse'; +import { GroupResponse } from "./groupResponse"; +import { UserResponse } from "./userResponse"; -import { SimResult } from '../simResult'; +import { SimResult } from "../simResult"; -import { BaseResponse } from 'jslib-node/cli/models/response/baseResponse'; +import { BaseResponse } from "jslib-node/cli/models/response/baseResponse"; export class TestResponse implements BaseResponse { - object: string; - groups: GroupResponse[] = []; - enabledUsers: UserResponse[] = []; - disabledUsers: UserResponse[] = []; - deletedUsers: UserResponse[] = []; + object: string; + groups: GroupResponse[] = []; + enabledUsers: UserResponse[] = []; + disabledUsers: UserResponse[] = []; + deletedUsers: UserResponse[] = []; - constructor(result: SimResult) { - this.object = 'test'; - this.groups = result.groups != null ? result.groups.map(g => new GroupResponse(g)) : []; - this.enabledUsers = result.enabledUsers != null ? result.enabledUsers.map(u => new UserResponse(u)) : []; - this.disabledUsers = result.disabledUsers != null ? result.disabledUsers.map(u => new UserResponse(u)) : []; - this.deletedUsers = result.deletedUsers != null ? result.deletedUsers.map(u => new UserResponse(u)) : []; - } + constructor(result: SimResult) { + this.object = "test"; + this.groups = result.groups != null ? result.groups.map((g) => new GroupResponse(g)) : []; + this.enabledUsers = + result.enabledUsers != null ? result.enabledUsers.map((u) => new UserResponse(u)) : []; + this.disabledUsers = + result.disabledUsers != null ? result.disabledUsers.map((u) => new UserResponse(u)) : []; + this.deletedUsers = + result.deletedUsers != null ? result.deletedUsers.map((u) => new UserResponse(u)) : []; + } } diff --git a/src/models/response/userResponse.ts b/src/models/response/userResponse.ts index aff5406c..ad1f51ad 100644 --- a/src/models/response/userResponse.ts +++ b/src/models/response/userResponse.ts @@ -1,11 +1,11 @@ -import { UserEntry } from '../userEntry'; +import { UserEntry } from "../userEntry"; export class UserResponse { - externalId: string; - displayName: string; + externalId: string; + displayName: string; - constructor(u: UserEntry) { - this.externalId = u.externalId; - this.displayName = u.displayName; - } + constructor(u: UserEntry) { + this.externalId = u.externalId; + this.displayName = u.displayName; + } } diff --git a/src/models/simResult.ts b/src/models/simResult.ts index 4adf301f..d4cdda0b 100644 --- a/src/models/simResult.ts +++ b/src/models/simResult.ts @@ -1,10 +1,10 @@ -import { GroupEntry } from './groupEntry'; -import { UserEntry } from './userEntry'; +import { GroupEntry } from "./groupEntry"; +import { UserEntry } from "./userEntry"; export class SimResult { - groups: GroupEntry[] = []; - users: UserEntry[] = []; - enabledUsers: UserEntry[] = []; - disabledUsers: UserEntry[] = []; - deletedUsers: UserEntry[] = []; + groups: GroupEntry[] = []; + users: UserEntry[] = []; + enabledUsers: UserEntry[] = []; + disabledUsers: UserEntry[] = []; + deletedUsers: UserEntry[] = []; } diff --git a/src/models/syncConfiguration.ts b/src/models/syncConfiguration.ts index 8b7fa634..fe2fa194 100644 --- a/src/models/syncConfiguration.ts +++ b/src/models/syncConfiguration.ts @@ -1,23 +1,23 @@ export class SyncConfiguration { - users = false; - groups = false; - interval = 5; - userFilter: string; - groupFilter: string; - removeDisabled = false; - overwriteExisting = false; - largeImport = false; - // Ldap properties - groupObjectClass: string; - userObjectClass: string; - groupPath: string; - userPath: string; - groupNameAttribute: string; - userEmailAttribute: string; - memberAttribute: string; - useEmailPrefixSuffix = false; - emailPrefixAttribute: string; - emailSuffix: string; - creationDateAttribute: string; - revisionDateAttribute: string; + users = false; + groups = false; + interval = 5; + userFilter: string; + groupFilter: string; + removeDisabled = false; + overwriteExisting = false; + largeImport = false; + // Ldap properties + groupObjectClass: string; + userObjectClass: string; + groupPath: string; + userPath: string; + groupNameAttribute: string; + userEmailAttribute: string; + memberAttribute: string; + useEmailPrefixSuffix = false; + emailPrefixAttribute: string; + emailSuffix: string; + creationDateAttribute: string; + revisionDateAttribute: string; } diff --git a/src/models/userEntry.ts b/src/models/userEntry.ts index a0ca272e..be9a61cf 100644 --- a/src/models/userEntry.ts +++ b/src/models/userEntry.ts @@ -1,15 +1,15 @@ -import { Entry } from './entry'; +import { Entry } from "./entry"; export class UserEntry extends Entry { - email: string; - disabled = false; - deleted = false; + email: string; + disabled = false; + deleted = false; - get displayName(): string { - if (this.email == null) { - return this.referenceId; - } - - return this.email; + get displayName(): string { + if (this.email == null) { + return this.referenceId; } + + return this.email; + } } diff --git a/src/program.ts b/src/program.ts index b578658e..7e009360 100644 --- a/src/program.ts +++ b/src/program.ts @@ -1,302 +1,330 @@ -import * as chalk from 'chalk'; -import * as program from 'commander'; -import * as path from 'path'; +import * as chalk from "chalk"; +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 { ConfigCommand } from './commands/config.command'; -import { LastSyncCommand } from './commands/lastSync.command'; -import { SyncCommand } from './commands/sync.command'; -import { TestCommand } from './commands/test.command'; +import { ClearCacheCommand } from "./commands/clearCache.command"; +import { ConfigCommand } from "./commands/config.command"; +import { LastSyncCommand } from "./commands/lastSync.command"; +import { SyncCommand } from "./commands/sync.command"; +import { TestCommand } from "./commands/test.command"; -import { LoginCommand } from 'jslib-node/cli/commands/login.command'; -import { LogoutCommand } from 'jslib-node/cli/commands/logout.command'; -import { UpdateCommand } from 'jslib-node/cli/commands/update.command'; +import { LoginCommand } from "jslib-node/cli/commands/login.command"; +import { LogoutCommand } from "jslib-node/cli/commands/logout.command"; +import { UpdateCommand } from "jslib-node/cli/commands/update.command"; -import { BaseProgram } from 'jslib-node/cli/baseProgram'; +import { BaseProgram } from "jslib-node/cli/baseProgram"; -import { ApiKeyService } from 'jslib-common/abstractions/apiKey.service'; -import { Response } from 'jslib-node/cli/models/response'; -import { StringResponse } from 'jslib-node/cli/models/response/stringResponse'; +import { ApiKeyService } from "jslib-common/abstractions/apiKey.service"; +import { Response } from "jslib-node/cli/models/response"; +import { StringResponse } from "jslib-node/cli/models/response/stringResponse"; -import { Utils } from 'jslib-common/misc/utils'; +import { Utils } from "jslib-common/misc/utils"; const writeLn = (s: string, finalLine: boolean = false, error: boolean = false) => { - const stream = error ? process.stderr : process.stdout; - if (finalLine && process.platform === 'win32') { - stream.write(s); - } else { - stream.write(s + '\n'); - } + const stream = error ? process.stderr : process.stdout; + if (finalLine && process.platform === "win32") { + stream.write(s); + } else { + stream.write(s + "\n"); + } }; export class Program extends BaseProgram { - private apiKeyService: ApiKeyService; + private apiKeyService: ApiKeyService; - constructor(private main: Main) { - super(main.userService, writeLn); - this.apiKeyService = main.apiKeyService; - } + constructor(private main: Main) { + super(main.userService, writeLn); + this.apiKeyService = main.apiKeyService; + } - async run() { - program - .option('--pretty', 'Format output. JSON is tabbed with two spaces.') - .option('--raw', 'Return raw output instead of a descriptive message.') - .option('--response', 'Return a JSON formatted version of response output.') - .option('--cleanexit', 'Exit with a success exit code (0) unless an error is thrown.') - .option('--quiet', 'Don\'t return anything to stdout.') - .option('--nointeraction', 'Do not prompt for interactive user input.') - .version(await this.main.platformUtilsService.getApplicationVersion(), '-v, --version'); + async run() { + program + .option("--pretty", "Format output. JSON is tabbed with two spaces.") + .option("--raw", "Return raw output instead of a descriptive message.") + .option("--response", "Return a JSON formatted version of response output.") + .option("--cleanexit", "Exit with a success exit code (0) unless an error is thrown.") + .option("--quiet", "Don't return anything to stdout.") + .option("--nointeraction", "Do not prompt for interactive user input.") + .version(await this.main.platformUtilsService.getApplicationVersion(), "-v, --version"); - program.on('option:pretty', () => { - process.env.BW_PRETTY = 'true'; - }); + program.on("option:pretty", () => { + process.env.BW_PRETTY = "true"; + }); - program.on('option:raw', () => { - process.env.BW_RAW = 'true'; - }); + program.on("option:raw", () => { + process.env.BW_RAW = "true"; + }); - program.on('option:quiet', () => { - process.env.BW_QUIET = 'true'; - }); + program.on("option:quiet", () => { + process.env.BW_QUIET = "true"; + }); - program.on('option:response', () => { - process.env.BW_RESPONSE = 'true'; - }); + program.on("option:response", () => { + process.env.BW_RESPONSE = "true"; + }); - program.on('option:cleanexit', () => { - process.env.BW_CLEANEXIT = 'true'; - }); + program.on("option:cleanexit", () => { + process.env.BW_CLEANEXIT = "true"; + }); - program.on('option:nointeraction', () => { - process.env.BW_NOINTERACTION = 'true'; - }); + program.on("option:nointeraction", () => { + process.env.BW_NOINTERACTION = "true"; + }); - program.on('command:*', () => { - writeLn(chalk.redBright('Invalid command: ' + program.args.join(' ')), false, true); - writeLn('See --help for a list of available commands.', true, true); - process.exitCode = 1; - }); + program.on("command:*", () => { + writeLn(chalk.redBright("Invalid command: " + program.args.join(" ")), false, true); + writeLn("See --help for a list of available commands.", true, true); + process.exitCode = 1; + }); - program.on('--help', () => { - writeLn('\n Examples:'); - writeLn(''); - writeLn(' bwdc login'); - writeLn(' bwdc test'); - writeLn(' bwdc sync'); - writeLn(' bwdc last-sync'); - writeLn(' bwdc config server https://bw.company.com'); - writeLn(' bwdc update'); - writeLn('', true); - }); + program.on("--help", () => { + writeLn("\n Examples:"); + writeLn(""); + writeLn(" bwdc login"); + writeLn(" bwdc test"); + writeLn(" bwdc sync"); + writeLn(" bwdc last-sync"); + writeLn(" bwdc config server https://bw.company.com"); + writeLn(" bwdc update"); + writeLn("", true); + }); - program - .command('login [clientId] [clientSecret]') - .description('Log into an organization account.', { - clientId: 'Client_id part of your organization\'s API key', - clientSecret: 'Client_secret part of your organization\'s API key', - }) - .action(async (clientId: string, clientSecret: string, options: program.OptionValues) => { - await this.exitIfAuthed(); - const command = new LoginCommand(this.main.authService, this.main.apiService, this.main.i18nService, - this.main.environmentService, this.main.passwordGenerationService, this.main.cryptoFunctionService, - this.main.platformUtilsService, this.main.userService, this.main.cryptoService, - this.main.policyService, 'connector', this.main.loginSyncService, this.main.keyConnectorService); + program + .command("login [clientId] [clientSecret]") + .description("Log into an organization account.", { + clientId: "Client_id part of your organization's API key", + clientSecret: "Client_secret part of your organization's API key", + }) + .action(async (clientId: string, clientSecret: string, options: program.OptionValues) => { + await this.exitIfAuthed(); + const command = new LoginCommand( + this.main.authService, + this.main.apiService, + this.main.i18nService, + this.main.environmentService, + this.main.passwordGenerationService, + this.main.cryptoFunctionService, + this.main.platformUtilsService, + this.main.userService, + this.main.cryptoService, + this.main.policyService, + "connector", + this.main.loginSyncService, + this.main.keyConnectorService + ); - if (!Utils.isNullOrWhitespace(clientId)) { - process.env.BW_CLIENTID = clientId; - } - if (!Utils.isNullOrWhitespace(clientSecret)) { - process.env.BW_CLIENTSECRET = clientSecret; - } - - options = Object.assign(options ?? {}, { apikey: true }); // force apikey use - const response = await command.run(null, null, options); - this.processResponse(response); - }); - - program - .command('logout') - .description('Log out of the current user account.') - .on('--help', () => { - writeLn('\n Examples:'); - writeLn(''); - writeLn(' bwdc logout'); - writeLn('', true); - }) - .action(async () => { - await this.exitIfNotAuthed(); - const command = new LogoutCommand(this.main.authService, this.main.i18nService, - async () => await this.main.logout()); - const response = await command.run(); - this.processResponse(response); - }); - - program - .command('test') - .description('Test a simulated sync.') - .option('-l, --last', 'Since the last successful sync.') - .on('--help', () => { - writeLn('\n Examples:'); - writeLn(''); - writeLn(' bwdc test'); - writeLn(' bwdc test --last'); - writeLn('', true); - }) - .action(async (options: program.OptionValues) => { - await this.exitIfNotAuthed(); - const command = new TestCommand(this.main.syncService, this.main.i18nService); - const response = await command.run(options); - this.processResponse(response); - }); - - program - .command('sync') - .description('Sync the directory.') - .on('--help', () => { - writeLn('\n Examples:'); - writeLn(''); - writeLn(' bwdc sync'); - writeLn('', true); - }) - .action(async () => { - await this.exitIfNotAuthed(); - const command = new SyncCommand(this.main.syncService, this.main.i18nService); - const response = await command.run(); - this.processResponse(response); - }); - - program - .command('last-sync ') - .description('Get the last successful sync date.') - .on('--help', () => { - writeLn('\n Notes:'); - writeLn(''); - writeLn(' Returns empty response if no sync has been performed for the given object.'); - writeLn(''); - writeLn(' Examples:'); - writeLn(''); - writeLn(' bwdc last-sync groups'); - writeLn(' bwdc last-sync users'); - writeLn('', true); - }) - .action(async (object: string) => { - await this.exitIfNotAuthed(); - const command = new LastSyncCommand(this.main.configurationService); - const response = await command.run(object); - this.processResponse(response); - }); - - program - .command('config [value]') - .description('Configure settings.') - .option('--secretenv ', 'Read secret from the named environment variable.') - .option('--secretfile ', 'Read secret from first line of the named file.') - .on('--help', () => { - writeLn('\n Settings:'); - writeLn(''); - writeLn(' server - On-premise hosted installation URL.'); - writeLn(' directory - The type of directory to use.'); - writeLn(' ldap.password - The password for connection to this LDAP server.'); - writeLn(' azure.key - The Azure AD secret key.'); - writeLn(' gsuite.key - The G Suite private key.'); - writeLn(' okta.token - The Okta token.'); - writeLn(' onelogin.secret - The OneLogin client secret.'); - writeLn(''); - writeLn(' Examples:'); - writeLn(''); - writeLn(' bwdc config server https://bw.company.com'); - writeLn(' bwdc config server bitwarden.com'); - writeLn(' bwdc config directory 1'); - writeLn(' bwdc config ldap.password '); - writeLn(' bwdc config ldap.password --secretenv LDAP_PWD'); - writeLn(' bwdc config azure.key '); - writeLn(' bwdc config gsuite.key '); - writeLn(' bwdc config okta.token '); - writeLn(' bwdc config onelogin.secret '); - writeLn('', true); - }) - .action(async (setting: string, value: string, options: program.OptionValues) => { - const command = new ConfigCommand(this.main.environmentService, this.main.i18nService, - this.main.configurationService); - const response = await command.run(setting, value, options); - this.processResponse(response); - }); - - program - .command('data-file') - .description('Path to data.json database file.') - .on('--help', () => { - writeLn('\n Examples:'); - writeLn(''); - writeLn(' bwdc data-file'); - writeLn('', true); - }) - .action(() => { - this.processResponse( - Response.success(new StringResponse(path.join(this.main.dataFilePath, 'data.json')))); - }); - - program - .command('clear-cache') - .description('Clear the sync cache.') - .on('--help', () => { - writeLn('\n Examples:'); - writeLn(''); - writeLn(' bwdc clear-cache'); - writeLn('', true); - }) - .action(async (options: program.OptionValues) => { - const command = new ClearCacheCommand(this.main.configurationService, this.main.i18nService); - const response = await command.run(options); - this.processResponse(response); - }); - - program - .command('update') - .description('Check for updates.') - .on('--help', () => { - writeLn('\n Notes:'); - writeLn(''); - writeLn(' Returns the URL to download the newest version of this CLI tool.'); - writeLn(''); - writeLn(' Use the `--raw` option to return only the download URL for the update.'); - writeLn(''); - writeLn(' Examples:'); - writeLn(''); - writeLn(' bwdc update'); - writeLn(' bwdc update --raw'); - writeLn('', true); - }) - .action(async () => { - const command = new UpdateCommand(this.main.platformUtilsService, this.main.i18nService, - 'directory-connector', 'bwdc', false); - const response = await command.run(); - this.processResponse(response); - }); - - program - .parse(process.argv); - - if (process.argv.slice(2).length === 0) { - program.outputHelp(); + if (!Utils.isNullOrWhitespace(clientId)) { + process.env.BW_CLIENTID = clientId; } - } - - async exitIfAuthed() { - const authed = await this.apiKeyService.isAuthenticated(); - if (authed) { - const type = await this.apiKeyService.getEntityType(); - const id = await this.apiKeyService.getEntityId(); - this.processResponse(Response.error('You are already logged in as ' + type + '.' + id + '.'), true); + if (!Utils.isNullOrWhitespace(clientSecret)) { + process.env.BW_CLIENTSECRET = clientSecret; } - } - async exitIfNotAuthed() { - const authed = await this.apiKeyService.isAuthenticated(); - if (!authed) { - this.processResponse(Response.error('You are not logged in.'), true); - } + options = Object.assign(options ?? {}, { apikey: true }); // force apikey use + const response = await command.run(null, null, options); + this.processResponse(response); + }); + + program + .command("logout") + .description("Log out of the current user account.") + .on("--help", () => { + writeLn("\n Examples:"); + writeLn(""); + writeLn(" bwdc logout"); + writeLn("", true); + }) + .action(async () => { + await this.exitIfNotAuthed(); + const command = new LogoutCommand( + this.main.authService, + this.main.i18nService, + async () => await this.main.logout() + ); + const response = await command.run(); + this.processResponse(response); + }); + + program + .command("test") + .description("Test a simulated sync.") + .option("-l, --last", "Since the last successful sync.") + .on("--help", () => { + writeLn("\n Examples:"); + writeLn(""); + writeLn(" bwdc test"); + writeLn(" bwdc test --last"); + writeLn("", true); + }) + .action(async (options: program.OptionValues) => { + await this.exitIfNotAuthed(); + const command = new TestCommand(this.main.syncService, this.main.i18nService); + const response = await command.run(options); + this.processResponse(response); + }); + + program + .command("sync") + .description("Sync the directory.") + .on("--help", () => { + writeLn("\n Examples:"); + writeLn(""); + writeLn(" bwdc sync"); + writeLn("", true); + }) + .action(async () => { + await this.exitIfNotAuthed(); + const command = new SyncCommand(this.main.syncService, this.main.i18nService); + const response = await command.run(); + this.processResponse(response); + }); + + program + .command("last-sync ") + .description("Get the last successful sync date.") + .on("--help", () => { + writeLn("\n Notes:"); + writeLn(""); + writeLn(" Returns empty response if no sync has been performed for the given object."); + writeLn(""); + writeLn(" Examples:"); + writeLn(""); + writeLn(" bwdc last-sync groups"); + writeLn(" bwdc last-sync users"); + writeLn("", true); + }) + .action(async (object: string) => { + await this.exitIfNotAuthed(); + const command = new LastSyncCommand(this.main.configurationService); + const response = await command.run(object); + this.processResponse(response); + }); + + program + .command("config [value]") + .description("Configure settings.") + .option("--secretenv ", "Read secret from the named environment variable.") + .option("--secretfile ", "Read secret from first line of the named file.") + .on("--help", () => { + writeLn("\n Settings:"); + writeLn(""); + writeLn(" server - On-premise hosted installation URL."); + writeLn(" directory - The type of directory to use."); + writeLn(" ldap.password - The password for connection to this LDAP server."); + writeLn(" azure.key - The Azure AD secret key."); + writeLn(" gsuite.key - The G Suite private key."); + writeLn(" okta.token - The Okta token."); + writeLn(" onelogin.secret - The OneLogin client secret."); + writeLn(""); + writeLn(" Examples:"); + writeLn(""); + writeLn(" bwdc config server https://bw.company.com"); + writeLn(" bwdc config server bitwarden.com"); + writeLn(" bwdc config directory 1"); + writeLn(" bwdc config ldap.password "); + writeLn(" bwdc config ldap.password --secretenv LDAP_PWD"); + writeLn(" bwdc config azure.key "); + writeLn(" bwdc config gsuite.key "); + writeLn(" bwdc config okta.token "); + writeLn(" bwdc config onelogin.secret "); + writeLn("", true); + }) + .action(async (setting: string, value: string, options: program.OptionValues) => { + const command = new ConfigCommand( + this.main.environmentService, + this.main.i18nService, + this.main.configurationService + ); + const response = await command.run(setting, value, options); + this.processResponse(response); + }); + + program + .command("data-file") + .description("Path to data.json database file.") + .on("--help", () => { + writeLn("\n Examples:"); + writeLn(""); + writeLn(" bwdc data-file"); + writeLn("", true); + }) + .action(() => { + this.processResponse( + Response.success(new StringResponse(path.join(this.main.dataFilePath, "data.json"))) + ); + }); + + program + .command("clear-cache") + .description("Clear the sync cache.") + .on("--help", () => { + writeLn("\n Examples:"); + writeLn(""); + writeLn(" bwdc clear-cache"); + writeLn("", true); + }) + .action(async (options: program.OptionValues) => { + const command = new ClearCacheCommand( + this.main.configurationService, + this.main.i18nService + ); + const response = await command.run(options); + this.processResponse(response); + }); + + program + .command("update") + .description("Check for updates.") + .on("--help", () => { + writeLn("\n Notes:"); + writeLn(""); + writeLn(" Returns the URL to download the newest version of this CLI tool."); + writeLn(""); + writeLn(" Use the `--raw` option to return only the download URL for the update."); + writeLn(""); + writeLn(" Examples:"); + writeLn(""); + writeLn(" bwdc update"); + writeLn(" bwdc update --raw"); + writeLn("", true); + }) + .action(async () => { + const command = new UpdateCommand( + this.main.platformUtilsService, + this.main.i18nService, + "directory-connector", + "bwdc", + false + ); + const response = await command.run(); + this.processResponse(response); + }); + + program.parse(process.argv); + + if (process.argv.slice(2).length === 0) { + program.outputHelp(); } + } + + async exitIfAuthed() { + const authed = await this.apiKeyService.isAuthenticated(); + if (authed) { + const type = await this.apiKeyService.getEntityType(); + const id = await this.apiKeyService.getEntityId(); + this.processResponse( + Response.error("You are already logged in as " + type + "." + id + "."), + true + ); + } + } + + async exitIfNotAuthed() { + const authed = await this.apiKeyService.isAuthenticated(); + if (!authed) { + this.processResponse(Response.error("You are not logged in."), true); + } + } } diff --git a/src/scss/bootstrap.scss b/src/scss/bootstrap.scss index 9170cf8c..11c8d2cf 100644 --- a/src/scss/bootstrap.scss +++ b/src/scss/bootstrap.scss @@ -1,5 +1,15 @@ -$theme-colors: ( "primary": #175DDC, "primary-accent": #1252A3, "danger": #dd4b39, "success": #00a65a, "info": #555555, "warning": #bf7e16, "secondary": #ced4da, "secondary-alt": #1A3B66); -$font-family-sans-serif: 'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; +$theme-colors: ( + "primary": #175ddc, + "primary-accent": #1252a3, + "danger": #dd4b39, + "success": #00a65a, + "info": #555555, + "warning": #bf7e16, + "secondary": #ced4da, + "secondary-alt": #1a3b66, +); +$font-family-sans-serif: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif, + "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; $h1-font-size: 2rem; $h2-font-size: 1.3rem; @@ -8,13 +18,13 @@ $h4-font-size: 1rem; $h5-font-size: 1rem; $h6-font-size: 1rem; -$primary: map_get($theme-colors, 'primary'); -$primary-accent: map_get($theme-colors, 'primary-accent'); -$success: map_get($theme-colors, 'success'); -$info: map_get($theme-colors, 'info'); -$warning: map_get($theme-colors, 'warning'); -$danger: map_get($theme-colors, 'danger'); -$secondary: map_get($theme-colors, 'secondary'); -$secondary-alt: map_get($theme-colors, 'secondary-alt'); +$primary: map_get($theme-colors, "primary"); +$primary-accent: map_get($theme-colors, "primary-accent"); +$success: map_get($theme-colors, "success"); +$info: map_get($theme-colors, "info"); +$warning: map_get($theme-colors, "warning"); +$danger: map_get($theme-colors, "danger"); +$secondary: map_get($theme-colors, "secondary"); +$secondary-alt: map_get($theme-colors, "secondary-alt"); @import "~bootstrap/scss/bootstrap.scss"; diff --git a/src/scss/environment.scss b/src/scss/environment.scss index cec22d19..add81976 100644 --- a/src/scss/environment.scss +++ b/src/scss/environment.scss @@ -1,7 +1,7 @@ @import "~bootstrap/scss/_variables.scss"; html.os_windows { - body { - border-top: 1px solid $gray-400; - } + body { + border-top: 1px solid $gray-400; + } } diff --git a/src/scss/misc.scss b/src/scss/misc.scss index 3c8e9fec..44866fc4 100644 --- a/src/scss/misc.scss +++ b/src/scss/misc.scss @@ -1,144 +1,143 @@ @import "~bootstrap/scss/_variables.scss"; body { - padding: 10px 0 20px 0; + padding: 10px 0 20px 0; } h1 { - border-bottom: 1px solid $border-color; - margin-bottom: 20px; + border-bottom: 1px solid $border-color; + margin-bottom: 20px; - small { - color: $text-muted; - font-size: $h1-font-size * .5; - } + small { + color: $text-muted; + font-size: $h1-font-size * 0.5; + } } h2 { - text-transform: uppercase; - font-weight: bold; + text-transform: uppercase; + font-weight: bold; } h3 { - text-transform: uppercase; + text-transform: uppercase; } h4 { - font-weight: bold; + font-weight: bold; } #duo-frame { - background: url('../images/loading.svg') 0 0 no-repeat; - height: 380px; + background: url("../images/loading.svg") 0 0 no-repeat; + height: 380px; - iframe { - width: 100%; - height: 100%; - border: none; - } + iframe { + width: 100%; + height: 100%; + border: none; + } } app-root > #loading { - text-align: center; - margin-top: 20px; - color: $text-muted; + text-align: center; + margin-top: 20px; + color: $text-muted; } ul.testing-list { - ul { - padding-left: 18px; - } + ul { + padding-left: 18px; + } - li.deleted { - text-decoration: line-through; - } + li.deleted { + text-decoration: line-through; + } } .callout { - padding: 10px; - margin-bottom: 10px; - border: 1px solid #000000; - border-left-width: 5px; - border-radius: 3px; - border-color: #ddd; - background-color: white; + padding: 10px; + margin-bottom: 10px; + border: 1px solid #000000; + border-left-width: 5px; + border-radius: 3px; + border-color: #ddd; + background-color: white; + + .callout-heading { + margin-top: 0; + } + + h3.callout-heading { + font-weight: bold; + text-transform: uppercase; + } + + &.callout-primary { + border-left-color: $primary; .callout-heading { - margin-top: 0; + color: $primary; } + } - h3.callout-heading { - font-weight: bold; - text-transform: uppercase; + &.callout-info { + border-left-color: $info; + + .callout-heading { + color: $info; } + } - &.callout-primary { - border-left-color: $primary; + &.callout-danger { + border-left-color: $danger; - .callout-heading { - color: $primary; - } + .callout-heading { + color: $danger; } + } - &.callout-info { - border-left-color: $info; + &.callout-success { + border-left-color: $success; - .callout-heading { - color: $info; - } + .callout-heading { + color: $success; } + } - &.callout-danger { - border-left-color: $danger; + &.callout-warning { + border-left-color: $warning; - .callout-heading { - color: $danger; - } + .callout-heading { + color: $warning; } + } - &.callout-success { - border-left-color: $success; - - .callout-heading { - color: $success; - } - } - - &.callout-warning { - border-left-color: $warning; - - .callout-heading { - color: $warning; - } - } - - ul { - padding-left: 40px; - margin: 0; - } + ul { + padding-left: 40px; + margin: 0; + } } .btn[class*="btn-outline-"] { - &:not(:hover) { - border-color: $secondary; - background-color: #fbfbfb; - } + &:not(:hover) { + border-color: $secondary; + background-color: #fbfbfb; + } } .btn-outline-secondary { - color: $text-muted; + color: $text-muted; - &:hover:not(:disabled) { - color: $body-color; - } + &:hover:not(:disabled) { + color: $body-color; + } - &:disabled { - opacity: 1; - } + &:disabled { + opacity: 1; + } - &:focus, - &.focus { - box-shadow: 0 0 0 $btn-focus-width rgba(mix(color-yiq($primary), $primary, 15%), .5); - } + &:focus, + &.focus { + box-shadow: 0 0 0 $btn-focus-width rgba(mix(color-yiq($primary), $primary, 15%), 0.5); + } } - diff --git a/src/scss/plugins.scss b/src/scss/plugins.scss index 274f09ed..5add8c62 100644 --- a/src/scss/plugins.scss +++ b/src/scss/plugins.scss @@ -1,127 +1,128 @@ $fa-font-path: "~font-awesome/fonts"; @import "~font-awesome/scss/font-awesome.scss"; -@import '~ngx-toastr/toastr'; +@import "~ngx-toastr/toastr"; @import "~bootstrap/scss/_variables.scss"; .toast-container { + .toast-close-button { + font-size: 18px; + margin-right: 4px; + } + + .ngx-toastr { + align-items: center; + background-image: none !important; + border-radius: $border-radius; + box-shadow: 0 0 8px rgba(0, 0, 0, 0.35); + display: flex; + padding: 15px; + .toast-close-button { - font-size: 18px; - margin-right: 4px; + position: absolute; + right: 5px; + top: 0; } - .ngx-toastr { - align-items: center; - background-image: none !important; - border-radius: $border-radius; - box-shadow: 0 0 8px rgba(0, 0, 0, 0.35); - display: flex; - padding: 15px; - - .toast-close-button { - position: absolute; - right: 5px; - top: 0; - } - - &:hover { - box-shadow: 0 0 10px rgba(0, 0, 0, 0.6); - } - - .icon i::before { - float: left; - font-style: normal; - font-family: FontAwesome; - font-size: 25px; - line-height: 20px; - padding-right: 15px; - } - - .toast-message { - p { - margin-bottom: 0.5rem; - - &:last-child { - margin-bottom: 0; - } - } - } - - &.toast-danger, &.toast-error { - background-color: $danger; - - .icon i::before { - content: "\f0e7"; - } - } - - &.toast-warning { - background-color: $warning; - - .icon i::before { - content: "\f071"; - } - } - - &.toast-info { - background-color: $info; - - .icon i:before { - content: "\f05a"; - } - } - - &.toast-success { - background-color: $success; - - .icon i:before { - content: "\f00C"; - } - } + &:hover { + box-shadow: 0 0 10px rgba(0, 0, 0, 0.6); } + + .icon i::before { + float: left; + font-style: normal; + font-family: FontAwesome; + font-size: 25px; + line-height: 20px; + padding-right: 15px; + } + + .toast-message { + p { + margin-bottom: 0.5rem; + + &:last-child { + margin-bottom: 0; + } + } + } + + &.toast-danger, + &.toast-error { + background-color: $danger; + + .icon i::before { + content: "\f0e7"; + } + } + + &.toast-warning { + background-color: $warning; + + .icon i::before { + content: "\f071"; + } + } + + &.toast-info { + background-color: $info; + + .icon i:before { + content: "\f05a"; + } + } + + &.toast-success { + background-color: $success; + + .icon i:before { + content: "\f00C"; + } + } + } } @keyframes modalshow { - 0% { - opacity: 0; - transform: translate(0, -25%); - } + 0% { + opacity: 0; + transform: translate(0, -25%); + } - 100% { - opacity: 1; - transform: translate(0, 0); - } + 100% { + opacity: 1; + transform: translate(0, 0); + } } @keyframes backdropshow { - 0% { - opacity: 0; - } + 0% { + opacity: 0; + } - 100% { - opacity: $modal-backdrop-opacity; - } + 100% { + opacity: $modal-backdrop-opacity; + } } .modal { - display: block !important; - opacity: 1 !important; + display: block !important; + opacity: 1 !important; } .modal-dialog { - .modal.fade & { - transform: initial !important; - animation: modalshow 0.3s ease-in; - } - .modal.show & { - transform: initial !important; - } - transform: translate(0, 0); + .modal.fade & { + transform: initial !important; + animation: modalshow 0.3s ease-in; + } + .modal.show & { + transform: initial !important; + } + transform: translate(0, 0); } .modal-backdrop { - &.fade { - animation: backdropshow 0.1s ease-in; - } - opacity: $modal-backdrop-opacity !important; + &.fade { + animation: backdropshow 0.1s ease-in; + } + opacity: $modal-backdrop-opacity !important; } diff --git a/src/services/api.service.ts b/src/services/api.service.ts index a0bf8ddc..f0342755 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -1,31 +1,36 @@ -import { ApiKeyService } from 'jslib-common/abstractions/apiKey.service'; -import { AuthService } from 'jslib-common/abstractions/auth.service'; -import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; -import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; -import { TokenService } from 'jslib-common/abstractions/token.service'; +import { ApiKeyService } from "jslib-common/abstractions/apiKey.service"; +import { AuthService } from "jslib-common/abstractions/auth.service"; +import { EnvironmentService } from "jslib-common/abstractions/environment.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; +import { TokenService } from "jslib-common/abstractions/token.service"; -import { ApiService as ApiServiceBase } from 'jslib-common/services/api.service'; +import { ApiService as ApiServiceBase } from "jslib-common/services/api.service"; export async function refreshToken(apiKeyService: ApiKeyService, authService: AuthService) { - try { - const clientId = await apiKeyService.getClientId(); - const clientSecret = await apiKeyService.getClientSecret(); - if (clientId != null && clientSecret != null) { - await authService.logInApiKey(clientId, clientSecret); - } - } catch (e) { - return Promise.reject(e); + try { + const clientId = await apiKeyService.getClientId(); + const clientSecret = await apiKeyService.getClientSecret(); + if (clientId != null && clientSecret != null) { + await authService.logInApiKey(clientId, clientSecret); } + } catch (e) { + return Promise.reject(e); + } } export class ApiService extends ApiServiceBase { - constructor(tokenService: TokenService, platformUtilsService: PlatformUtilsService, environmentService: EnvironmentService, - private refreshTokenCallback: () => Promise, logoutCallback: (expired: boolean) => Promise, - customUserAgent: string = null) { - super(tokenService, platformUtilsService, environmentService, logoutCallback, customUserAgent); - } + constructor( + tokenService: TokenService, + platformUtilsService: PlatformUtilsService, + environmentService: EnvironmentService, + private refreshTokenCallback: () => Promise, + logoutCallback: (expired: boolean) => Promise, + customUserAgent: string = null + ) { + super(tokenService, platformUtilsService, environmentService, logoutCallback, customUserAgent); + } - doRefreshToken(): Promise { - return this.refreshTokenCallback(); - } + doRefreshToken(): Promise { + return this.refreshTokenCallback(); + } } diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index 43546125..7748d9de 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -1,66 +1,96 @@ -import { ApiService } from 'jslib-common/abstractions/api.service'; -import { ApiKeyService } from 'jslib-common/abstractions/apiKey.service'; -import { AppIdService } from 'jslib-common/abstractions/appId.service'; -import { CryptoService } from 'jslib-common/abstractions/crypto.service'; -import { CryptoFunctionService } from 'jslib-common/abstractions/cryptoFunction.service'; -import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { KeyConnectorService } from 'jslib-common/abstractions/keyConnector.service'; -import { LogService } from 'jslib-common/abstractions/log.service'; -import { MessagingService } from 'jslib-common/abstractions/messaging.service'; -import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; -import { TokenService } from 'jslib-common/abstractions/token.service'; -import { UserService } from 'jslib-common/abstractions/user.service'; -import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.service'; +import { ApiService } from "jslib-common/abstractions/api.service"; +import { ApiKeyService } from "jslib-common/abstractions/apiKey.service"; +import { AppIdService } from "jslib-common/abstractions/appId.service"; +import { CryptoService } from "jslib-common/abstractions/crypto.service"; +import { CryptoFunctionService } from "jslib-common/abstractions/cryptoFunction.service"; +import { EnvironmentService } from "jslib-common/abstractions/environment.service"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { KeyConnectorService } from "jslib-common/abstractions/keyConnector.service"; +import { LogService } from "jslib-common/abstractions/log.service"; +import { MessagingService } from "jslib-common/abstractions/messaging.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; +import { TokenService } from "jslib-common/abstractions/token.service"; +import { UserService } from "jslib-common/abstractions/user.service"; +import { VaultTimeoutService } from "jslib-common/abstractions/vaultTimeout.service"; -import { AuthService as AuthServiceBase } from 'jslib-common/services/auth.service'; +import { AuthService as AuthServiceBase } from "jslib-common/services/auth.service"; -import { AuthResult } from 'jslib-common/models/domain/authResult'; -import { DeviceRequest } from 'jslib-common/models/request/deviceRequest'; -import { TokenRequest } from 'jslib-common/models/request/tokenRequest'; -import { IdentityTokenResponse } from 'jslib-common/models/response/identityTokenResponse'; +import { AuthResult } from "jslib-common/models/domain/authResult"; +import { DeviceRequest } from "jslib-common/models/request/deviceRequest"; +import { TokenRequest } from "jslib-common/models/request/tokenRequest"; +import { IdentityTokenResponse } from "jslib-common/models/response/identityTokenResponse"; export class AuthService extends AuthServiceBase { + constructor( + cryptoService: CryptoService, + apiService: ApiService, + userService: UserService, + tokenService: TokenService, + appIdService: AppIdService, + i18nService: I18nService, + platformUtilsService: PlatformUtilsService, + messagingService: MessagingService, + vaultTimeoutService: VaultTimeoutService, + logService: LogService, + private apiKeyService: ApiKeyService, + cryptoFunctionService: CryptoFunctionService, + environmentService: EnvironmentService, + keyConnectorService: KeyConnectorService + ) { + super( + cryptoService, + apiService, + userService, + tokenService, + appIdService, + i18nService, + platformUtilsService, + messagingService, + vaultTimeoutService, + logService, + cryptoFunctionService, + environmentService, + keyConnectorService, + false + ); + } - constructor(cryptoService: CryptoService, apiService: ApiService, userService: UserService, - tokenService: TokenService, appIdService: AppIdService, i18nService: I18nService, - platformUtilsService: PlatformUtilsService, messagingService: MessagingService, - vaultTimeoutService: VaultTimeoutService, logService: LogService, private apiKeyService: ApiKeyService, - cryptoFunctionService: CryptoFunctionService, environmentService: EnvironmentService, - keyConnectorService: KeyConnectorService) { - super(cryptoService, apiService, userService, tokenService, appIdService, i18nService, platformUtilsService, - messagingService, vaultTimeoutService, logService, cryptoFunctionService, environmentService, - keyConnectorService, false); + async logInApiKey(clientId: string, clientSecret: string): Promise { + this.selectedTwoFactorProviderType = null; + if (clientId.startsWith("organization")) { + return await this.organizationLogInHelper(clientId, clientSecret); } + return await super.logInApiKey(clientId, clientSecret); + } - async logInApiKey(clientId: string, clientSecret: string): Promise { - this.selectedTwoFactorProviderType = null; - if (clientId.startsWith('organization')) { - return await this.organizationLogInHelper(clientId, clientSecret); - } - return await super.logInApiKey(clientId, clientSecret); - } + async logOut(callback: Function) { + this.apiKeyService.clear(); + super.logOut(callback); + } - async logOut(callback: Function) { - this.apiKeyService.clear(); - super.logOut(callback); - } + private async organizationLogInHelper(clientId: string, clientSecret: string) { + const appId = await this.appIdService.getAppId(); + const deviceRequest = new DeviceRequest(appId, this.platformUtilsService); + const request = new TokenRequest( + null, + null, + [clientId, clientSecret], + null, + null, + false, + null, + deviceRequest + ); - private async organizationLogInHelper(clientId: string, clientSecret: string) { - const appId = await this.appIdService.getAppId(); - const deviceRequest = new DeviceRequest(appId, this.platformUtilsService); - const request = new TokenRequest(null, null, [clientId, clientSecret], null, - null, false, null, deviceRequest); + const response = await this.apiService.postIdentityToken(request); + const result = new AuthResult(); + result.twoFactor = !(response as any).accessToken; - const response = await this.apiService.postIdentityToken(request); - const result = new AuthResult(); - result.twoFactor = !(response as any).accessToken; + const tokenResponse = response as IdentityTokenResponse; + result.resetMasterPassword = tokenResponse.resetMasterPassword; + await this.tokenService.setToken(tokenResponse.accessToken); + await this.apiKeyService.setInformation(clientId, clientSecret); - const tokenResponse = response as IdentityTokenResponse; - result.resetMasterPassword = tokenResponse.resetMasterPassword; - await this.tokenService.setToken(tokenResponse.accessToken); - await this.apiKeyService.setInformation(clientId, clientSecret); - - return result; - } + return result; + } } diff --git a/src/services/azure-directory.service.ts b/src/services/azure-directory.service.ts index 3e95f644..4e5ea2e8 100644 --- a/src/services/azure-directory.service.ts +++ b/src/services/azure-directory.service.ts @@ -1,473 +1,500 @@ -import * as graph from '@microsoft/microsoft-graph-client'; -import * as graphType from '@microsoft/microsoft-graph-types'; -import * as https from 'https'; -import * as querystring from 'querystring'; +import * as graph from "@microsoft/microsoft-graph-client"; +import * as graphType from "@microsoft/microsoft-graph-types"; +import * as https from "https"; +import * as querystring from "querystring"; -import { DirectoryType } from '../enums/directoryType'; +import { DirectoryType } from "../enums/directoryType"; -import { AzureConfiguration } from '../models/azureConfiguration'; -import { GroupEntry } from '../models/groupEntry'; -import { SyncConfiguration } from '../models/syncConfiguration'; -import { UserEntry } from '../models/userEntry'; +import { AzureConfiguration } from "../models/azureConfiguration"; +import { GroupEntry } from "../models/groupEntry"; +import { SyncConfiguration } from "../models/syncConfiguration"; +import { UserEntry } from "../models/userEntry"; -import { BaseDirectoryService } from './baseDirectory.service'; -import { ConfigurationService } from './configuration.service'; -import { IDirectoryService } from './directory.service'; +import { BaseDirectoryService } from "./baseDirectory.service"; +import { ConfigurationService } from "./configuration.service"; +import { IDirectoryService } from "./directory.service"; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { LogService } from 'jslib-common/abstractions/log.service'; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { LogService } from "jslib-common/abstractions/log.service"; -const AzurePublicIdentityAuhtority = 'login.microsoftonline.com'; -const AzureGovermentIdentityAuhtority = 'login.microsoftonline.us'; +const AzurePublicIdentityAuhtority = "login.microsoftonline.com"; +const AzureGovermentIdentityAuhtority = "login.microsoftonline.us"; -const NextLink = '@odata.nextLink'; -const DeltaLink = '@odata.deltaLink'; -const ObjectType = '@odata.type'; -const UserSelectParams = '?$select=id,mail,userPrincipalName,displayName,accountEnabled'; +const NextLink = "@odata.nextLink"; +const DeltaLink = "@odata.deltaLink"; +const ObjectType = "@odata.type"; +const UserSelectParams = "?$select=id,mail,userPrincipalName,displayName,accountEnabled"; enum UserSetType { - IncludeUser, - ExcludeUser, - IncludeGroup, - ExcludeGroup, + IncludeUser, + ExcludeUser, + IncludeGroup, + ExcludeGroup, } export class AzureDirectoryService extends BaseDirectoryService implements IDirectoryService { - private client: graph.Client; - private dirConfig: AzureConfiguration; - private syncConfig: SyncConfiguration; - private accessToken: string; - private accessTokenExpiration: Date; + private client: graph.Client; + private dirConfig: AzureConfiguration; + private syncConfig: SyncConfiguration; + private accessToken: string; + private accessTokenExpiration: Date; - constructor(private configurationService: ConfigurationService, private logService: LogService, - private i18nService: I18nService) { - super(); - this.init(); + constructor( + private configurationService: ConfigurationService, + private logService: LogService, + private i18nService: I18nService + ) { + super(); + this.init(); + } + + async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> { + const type = await this.configurationService.getDirectoryType(); + if (type !== DirectoryType.AzureActiveDirectory) { + return; } - async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> { - const type = await this.configurationService.getDirectoryType(); - if (type !== DirectoryType.AzureActiveDirectory) { - return; - } - - this.dirConfig = await this.configurationService.getDirectory( - DirectoryType.AzureActiveDirectory); - if (this.dirConfig == null) { - return; - } - - this.syncConfig = await this.configurationService.getSync(); - if (this.syncConfig == null) { - return; - } - - let users: UserEntry[]; - if (this.syncConfig.users) { - users = await this.getCurrentUsers(); - const deletedUsers = await this.getDeletedUsers(force, !test); - users = users.concat(deletedUsers); - } - - let groups: GroupEntry[]; - if (this.syncConfig.groups) { - const setFilter = await this.createAadCustomSet(this.syncConfig.groupFilter); - groups = await this.getGroups(setFilter); - users = this.filterUsersFromGroupsSet(users, groups, setFilter, this.syncConfig); - } - - return [groups, users]; + this.dirConfig = await this.configurationService.getDirectory( + DirectoryType.AzureActiveDirectory + ); + if (this.dirConfig == null) { + return; } - private async getCurrentUsers(): Promise { - const entryIds = new Set(); - const entries: UserEntry[] = []; - const userReq = this.client.api('/users' + UserSelectParams); - let res = await userReq.get(); - const setFilter = this.createCustomUserSet(this.syncConfig.userFilter); - while (true) { - const users: graphType.User[] = res.value; - if (users != null) { - for (const user of users) { - if (user.id == null || entryIds.has(user.id)) { - continue; - } - const entry = this.buildUser(user); - if (await this.filterOutUserResult(setFilter, entry, true)) { - continue; - } - - if (!entry.disabled && !entry.deleted && - (entry.email == null || entry.email.indexOf('#') > -1)) { - continue; - } - - entries.push(entry); - entryIds.add(user.id); - } - } - - if (res[NextLink] == null) { - break; - } else { - const nextReq = this.client.api(res[NextLink]); - res = await nextReq.get(); - } - } - - return entries; + this.syncConfig = await this.configurationService.getSync(); + if (this.syncConfig == null) { + return; } - private async getDeletedUsers(force: boolean, saveDelta: boolean): Promise { - const entryIds = new Set(); - 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' + UserSelectParams); - 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.deleted) { - continue; - } - if (await this.filterOutUserResult(setFilter, entry, false)) { - 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; + let users: UserEntry[]; + if (this.syncConfig.users) { + users = await this.getCurrentUsers(); + const deletedUsers = await this.getDeletedUsers(force, !test); + users = users.concat(deletedUsers); } - private async createAadCustomSet(filter: string): Promise<[boolean, Set]> { - if (filter == null || filter === '') { - return null; - } - - const mainParts = filter.split('|'); - if (mainParts.length < 1 || mainParts[0] == null || mainParts[0].trim() === '') { - return null; - } - - const parts = mainParts[0].split(':'); - if (parts.length !== 2) { - return null; - } - - const keyword = parts[0].trim().toLowerCase(); - let exclude = true; - if (keyword === 'include') { - exclude = false; - } else if (keyword === 'exclude') { - exclude = true; - } else if (keyword === 'excludeadministrativeunit') { - exclude = true; - } else if (keyword === 'includeadministrativeunit') { - exclude = false; - } else { - return null; - } - - const set = new Set(); - const pieces = parts[1].split(','); - if (keyword === 'excludeadministrativeunit' || keyword === 'includeadministrativeunit') { - for (const p of pieces) { - const auMembers = await this.client - .api(`https://graph.microsoft.com/v1.0/directory/administrativeUnits/${p}/members`).get(); - for (const auMember of auMembers.value) { - if (auMember['@odata.type'] === '#microsoft.graph.group') { - set.add(auMember.displayName.toLowerCase()); - } - } - } - } else { - for (const p of pieces) { - set.add(p.trim().toLowerCase()); - } - } - return [exclude, set]; + let groups: GroupEntry[]; + if (this.syncConfig.groups) { + const setFilter = await this.createAadCustomSet(this.syncConfig.groupFilter); + groups = await this.getGroups(setFilter); + users = this.filterUsersFromGroupsSet(users, groups, setFilter, this.syncConfig); } - private createCustomUserSet(filter: string): [UserSetType, Set] { - if (filter == null || filter === '') { - return null; - } + return [groups, users]; + } - const mainParts = filter.split('|'); - if (mainParts.length < 1 || mainParts[0] == null || mainParts[0].trim() === '') { - return null; - } + private async getCurrentUsers(): Promise { + const entryIds = new Set(); + const entries: UserEntry[] = []; + const userReq = this.client.api("/users" + UserSelectParams); + let res = await userReq.get(); + const setFilter = this.createCustomUserSet(this.syncConfig.userFilter); + while (true) { + const users: graphType.User[] = res.value; + if (users != null) { + for (const user of users) { + if (user.id == null || entryIds.has(user.id)) { + continue; + } + const entry = this.buildUser(user); + if (await this.filterOutUserResult(setFilter, entry, true)) { + continue; + } - const parts = mainParts[0].split(':'); - if (parts.length !== 2) { - return null; - } + if ( + !entry.disabled && + !entry.deleted && + (entry.email == null || entry.email.indexOf("#") > -1) + ) { + continue; + } - const keyword = parts[0].trim().toLowerCase(); - let userSetType = UserSetType.IncludeUser; - if (keyword === 'include') { - userSetType = UserSetType.IncludeUser; - } else if (keyword === 'exclude') { - userSetType = UserSetType.ExcludeUser; - } else if (keyword === 'includegroup') { - userSetType = UserSetType.IncludeGroup; - } else if (keyword === 'excludegroup') { - userSetType = UserSetType.ExcludeGroup; - } else { - return null; + entries.push(entry); + entryIds.add(user.id); } + } - const set = new Set(); - const pieces = parts[1].split(','); - for (const p of pieces) { - set.add(p.trim().toLowerCase()); - } - - return [userSetType, set]; + if (res[NextLink] == null) { + break; + } else { + const nextReq = this.client.api(res[NextLink]); + res = await nextReq.get(); + } } - private async filterOutUserResult(setFilter: [UserSetType, Set], user: UserEntry, - checkGroupsFilter: boolean): Promise { - if (setFilter == null) { - return false; + return entries; + } + + private async getDeletedUsers(force: boolean, saveDelta: boolean): Promise { + const entryIds = new Set(); + 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" + UserSelectParams); + 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.deleted) { + continue; + } + if (await this.filterOutUserResult(setFilter, entry, false)) { + 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 async createAadCustomSet(filter: string): Promise<[boolean, Set]> { + if (filter == null || filter === "") { + return null; + } + + const mainParts = filter.split("|"); + if (mainParts.length < 1 || mainParts[0] == null || mainParts[0].trim() === "") { + return null; + } + + const parts = mainParts[0].split(":"); + if (parts.length !== 2) { + return null; + } + + const keyword = parts[0].trim().toLowerCase(); + let exclude = true; + if (keyword === "include") { + exclude = false; + } else if (keyword === "exclude") { + exclude = true; + } else if (keyword === "excludeadministrativeunit") { + exclude = true; + } else if (keyword === "includeadministrativeunit") { + exclude = false; + } else { + return null; + } + + const set = new Set(); + const pieces = parts[1].split(","); + if (keyword === "excludeadministrativeunit" || keyword === "includeadministrativeunit") { + for (const p of pieces) { + const auMembers = await this.client + .api(`https://graph.microsoft.com/v1.0/directory/administrativeUnits/${p}/members`) + .get(); + for (const auMember of auMembers.value) { + if (auMember["@odata.type"] === "#microsoft.graph.group") { + set.add(auMember.displayName.toLowerCase()); + } + } + } + } else { + for (const p of pieces) { + set.add(p.trim().toLowerCase()); + } + } + return [exclude, set]; + } + + private createCustomUserSet(filter: string): [UserSetType, Set] { + if (filter == null || filter === "") { + return null; + } + + const mainParts = filter.split("|"); + if (mainParts.length < 1 || mainParts[0] == null || mainParts[0].trim() === "") { + return null; + } + + const parts = mainParts[0].split(":"); + if (parts.length !== 2) { + return null; + } + + const keyword = parts[0].trim().toLowerCase(); + let userSetType = UserSetType.IncludeUser; + if (keyword === "include") { + userSetType = UserSetType.IncludeUser; + } else if (keyword === "exclude") { + userSetType = UserSetType.ExcludeUser; + } else if (keyword === "includegroup") { + userSetType = UserSetType.IncludeGroup; + } else if (keyword === "excludegroup") { + userSetType = UserSetType.ExcludeGroup; + } else { + return null; + } + + const set = new Set(); + const pieces = parts[1].split(","); + for (const p of pieces) { + set.add(p.trim().toLowerCase()); + } + + return [userSetType, set]; + } + + private async filterOutUserResult( + setFilter: [UserSetType, Set], + user: UserEntry, + checkGroupsFilter: boolean + ): Promise { + if (setFilter == null) { + return false; + } + + let userSetTypeExclude = null; + if (setFilter[0] === UserSetType.IncludeUser) { + userSetTypeExclude = false; + } else if (setFilter[0] === UserSetType.ExcludeUser) { + userSetTypeExclude = true; + } + + if (userSetTypeExclude != null) { + return this.filterOutResult([userSetTypeExclude, setFilter[1]], user.email); + } + + // We need to *not* call the /checkMemberGroups method for deleted users, it will always fail + if (!checkGroupsFilter) { + return false; + } + const memberGroups = await this.client.api(`/users/${user.externalId}/checkMemberGroups`).post({ + groupIds: Array.from(setFilter[1]), + }); + if (memberGroups.value.length > 0 && setFilter[0] === UserSetType.IncludeGroup) { + return false; + } else if (memberGroups.value.length > 0 && setFilter[0] === UserSetType.ExcludeGroup) { + return true; + } else if (memberGroups.value.length === 0 && setFilter[0] === UserSetType.IncludeGroup) { + return true; + } else if (memberGroups.value.length === 0 && setFilter[0] === UserSetType.ExcludeGroup) { + return false; + } + + return false; + } + + private buildUser(user: graphType.User): UserEntry { + const entry = new UserEntry(); + entry.referenceId = user.id; + entry.externalId = user.id; + entry.email = user.mail; + + if ( + user.userPrincipalName && + (entry.email == null || entry.email === "" || entry.email.indexOf("onmicrosoft.com") > -1) + ) { + entry.email = user.userPrincipalName; + } + + if (entry.email != null) { + entry.email = entry.email.trim().toLowerCase(); + } + + entry.disabled = user.accountEnabled == null ? false : !user.accountEnabled; + + if ((user as any)["@removed"] != null && (user as any)["@removed"].reason === "changed") { + entry.deleted = true; + } + + return entry; + } + + private async getGroups(setFilter: [boolean, Set]): Promise { + const entryIds = new Set(); + const entries: GroupEntry[] = []; + const groupsReq = this.client.api("/groups"); + let res = await groupsReq.get(); + while (true) { + const groups: graphType.Group[] = res.value; + if (groups != null) { + for (const group of groups) { + if (group.id == null || entryIds.has(group.id)) { + continue; + } + if (this.filterOutResult(setFilter, group.displayName)) { + continue; + } + + const entry = await this.buildGroup(group); + entries.push(entry); + entryIds.add(group.id); + } + } + + if (res[NextLink] == null) { + break; + } else { + const nextReq = this.client.api(res[NextLink]); + res = await nextReq.get(); + } + } + + return entries; + } + + private async buildGroup(group: graphType.Group): Promise { + const entry = new GroupEntry(); + entry.referenceId = group.id; + entry.externalId = group.id; + entry.name = group.displayName; + + const memReq = this.client.api("/groups/" + group.id + "/members"); + let memRes = await memReq.get(); + while (true) { + const members: any = memRes.value; + if (members != null) { + for (const member of members) { + if (member[ObjectType] === "#microsoft.graph.group") { + entry.groupMemberReferenceIds.add((member as graphType.Group).id); + } else if (member[ObjectType] === "#microsoft.graph.user") { + entry.userMemberExternalIds.add((member as graphType.User).id); + } + } + } + if (memRes[NextLink] == null) { + break; + } else { + const nextMemReq = this.client.api(memRes[NextLink]); + memRes = await nextMemReq.get(); + } + } + + return entry; + } + + private init() { + this.client = graph.Client.init({ + authProvider: (done) => { + if ( + this.dirConfig.applicationId == null || + this.dirConfig.key == null || + this.dirConfig.tenant == null + ) { + done(new Error(this.i18nService.t("dirConfigIncomplete")), null); + return; } - let userSetTypeExclude = null; - if (setFilter[0] === UserSetType.IncludeUser) { - userSetTypeExclude = false; - } else if (setFilter[0] === UserSetType.ExcludeUser) { - userSetTypeExclude = true; + const identityAuthority = + this.dirConfig.identityAuthority != null + ? this.dirConfig.identityAuthority + : AzurePublicIdentityAuhtority; + if ( + identityAuthority !== AzurePublicIdentityAuhtority && + identityAuthority !== AzureGovermentIdentityAuhtority + ) { + done(new Error(this.i18nService.t("dirConfigIncomplete")), null); + return; } - if (userSetTypeExclude != null) { - return this.filterOutResult([userSetTypeExclude, setFilter[1]], user.email); + if (!this.accessTokenIsExpired()) { + done(null, this.accessToken); + return; } - // We need to *not* call the /checkMemberGroups method for deleted users, it will always fail - if (!checkGroupsFilter) { - return false; - } - const memberGroups = await this.client.api(`/users/${user.externalId}/checkMemberGroups`).post({ - groupIds: Array.from(setFilter[1]), + this.accessToken = null; + this.accessTokenExpiration = null; + + const data = querystring.stringify({ + client_id: this.dirConfig.applicationId, + client_secret: this.dirConfig.key, + grant_type: "client_credentials", + scope: "https://graph.microsoft.com/.default", }); - if (memberGroups.value.length > 0 && setFilter[0] === UserSetType.IncludeGroup) { - return false; - } else if (memberGroups.value.length > 0 && setFilter[0] === UserSetType.ExcludeGroup) { - return true; - } else if (memberGroups.value.length === 0 && setFilter[0] === UserSetType.IncludeGroup) { - return true; - } else if (memberGroups.value.length === 0 && setFilter[0] === UserSetType.ExcludeGroup) { - return false; - } - return false; - } - - private buildUser(user: graphType.User): UserEntry { - const entry = new UserEntry(); - entry.referenceId = user.id; - entry.externalId = user.id; - entry.email = user.mail; - - if (user.userPrincipalName && (entry.email == null || entry.email === '' || - entry.email.indexOf('onmicrosoft.com') > -1)) { - entry.email = user.userPrincipalName; - } - - if (entry.email != null) { - entry.email = entry.email.trim().toLowerCase(); - } - - entry.disabled = user.accountEnabled == null ? false : !user.accountEnabled; - - if ((user as any)['@removed'] != null && (user as any)['@removed'].reason === 'changed') { - entry.deleted = true; - } - - return entry; - } - - private async getGroups(setFilter: [boolean, Set]): Promise { - const entryIds = new Set(); - const entries: GroupEntry[] = []; - const groupsReq = this.client.api('/groups'); - let res = await groupsReq.get(); - while (true) { - const groups: graphType.Group[] = res.value; - if (groups != null) { - for (const group of groups) { - if (group.id == null || entryIds.has(group.id)) { - continue; - } - if (this.filterOutResult(setFilter, group.displayName)) { - continue; - } - - const entry = await this.buildGroup(group); - entries.push(entry); - entryIds.add(group.id); - } - } - - if (res[NextLink] == null) { - break; - } else { - const nextReq = this.client.api(res[NextLink]); - res = await nextReq.get(); - } - } - - return entries; - } - - private async buildGroup(group: graphType.Group): Promise { - const entry = new GroupEntry(); - entry.referenceId = group.id; - entry.externalId = group.id; - entry.name = group.displayName; - - const memReq = this.client.api('/groups/' + group.id + '/members'); - let memRes = await memReq.get(); - while (true) { - const members: any = memRes.value; - if (members != null) { - for (const member of members) { - if (member[ObjectType] === '#microsoft.graph.group') { - entry.groupMemberReferenceIds.add((member as graphType.Group).id); - } else if (member[ObjectType] === '#microsoft.graph.user') { - entry.userMemberExternalIds.add((member as graphType.User).id); - } - } - } - if (memRes[NextLink] == null) { - break; - } else { - const nextMemReq = this.client.api(memRes[NextLink]); - memRes = await nextMemReq.get(); - } - } - - return entry; - } - - private init() { - this.client = graph.Client.init({ - authProvider: done => { - if (this.dirConfig.applicationId == null || this.dirConfig.key == null || - this.dirConfig.tenant == null) { - done(new Error(this.i18nService.t('dirConfigIncomplete')), null); - return; - } - - const identityAuthority = this.dirConfig.identityAuthority != null ? this.dirConfig.identityAuthority : AzurePublicIdentityAuhtority; - if (identityAuthority !== AzurePublicIdentityAuhtority && identityAuthority !== AzureGovermentIdentityAuhtority) { - done(new Error(this.i18nService.t('dirConfigIncomplete')), null); - return; - } - - if (!this.accessTokenIsExpired()) { - done(null, this.accessToken); - return; - } - - this.accessToken = null; - this.accessTokenExpiration = null; - - const data = querystring.stringify({ - client_id: this.dirConfig.applicationId, - client_secret: this.dirConfig.key, - grant_type: 'client_credentials', - scope: 'https://graph.microsoft.com/.default', - }); - - const req = https.request({ - host: identityAuthority, - path: '/' + this.dirConfig.tenant + '/oauth2/v2.0/token', - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Content-Length': Buffer.byteLength(data), - }, - }, res => { - res.setEncoding('utf8'); - res.on('data', (chunk: string) => { - const d = JSON.parse(chunk); - if (res.statusCode === 200 && d.access_token != null) { - this.setAccessTokenExpiration(d.access_token, d.expires_in); - done(null, d.access_token); - } else if (d.error != null && d.error_description != null) { - const shortError = d.error_description?.split('\n', 1)[0]; - const err = new Error(d.error + ' (' + res.statusCode + '): ' + shortError); - // tslint:disable-next-line - console.error(d.error_description); - done(err, null); - } else { - const err = new Error('Unknown error (' + res.statusCode + ').'); - done(err, null); - } - }); - }).on('error', err => { - done(err, null); - }); - - req.write(data); - req.end(); + const req = https + .request( + { + host: identityAuthority, + path: "/" + this.dirConfig.tenant + "/oauth2/v2.0/token", + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + "Content-Length": Buffer.byteLength(data), + }, }, - }); + (res) => { + res.setEncoding("utf8"); + res.on("data", (chunk: string) => { + const d = JSON.parse(chunk); + if (res.statusCode === 200 && d.access_token != null) { + this.setAccessTokenExpiration(d.access_token, d.expires_in); + done(null, d.access_token); + } else if (d.error != null && d.error_description != null) { + const shortError = d.error_description?.split("\n", 1)[0]; + const err = new Error(d.error + " (" + res.statusCode + "): " + shortError); + // tslint:disable-next-line + console.error(d.error_description); + done(err, null); + } else { + const err = new Error("Unknown error (" + res.statusCode + ")."); + done(err, null); + } + }); + } + ) + .on("error", (err) => { + done(err, null); + }); + + req.write(data); + req.end(); + }, + }); + } + + private accessTokenIsExpired() { + if (this.accessToken == null || this.accessTokenExpiration == null) { + return true; } - private accessTokenIsExpired() { - if (this.accessToken == null || this.accessTokenExpiration == null) { - return true; - } + // expired if less than 2 minutes til expiration + const now = new Date(); + return this.accessTokenExpiration.getTime() - now.getTime() < 120000; + } - // expired if less than 2 minutes til expiration - const now = new Date(); - return this.accessTokenExpiration.getTime() - now.getTime() < 120000; + private setAccessTokenExpiration(accessToken: string, expSeconds: number) { + if (accessToken == null || expSeconds == null) { + return; } - private setAccessTokenExpiration(accessToken: string, expSeconds: number) { - if (accessToken == null || expSeconds == null) { - return; - } - - this.accessToken = accessToken; - const exp = new Date(); - exp.setSeconds(exp.getSeconds() + expSeconds); - this.accessTokenExpiration = exp; - } + this.accessToken = accessToken; + const exp = new Date(); + exp.setSeconds(exp.getSeconds() + expSeconds); + this.accessTokenExpiration = exp; + } } diff --git a/src/services/baseDirectory.service.ts b/src/services/baseDirectory.service.ts index c736c448..d9106f4e 100644 --- a/src/services/baseDirectory.service.ts +++ b/src/services/baseDirectory.service.ts @@ -1,91 +1,95 @@ -import { SyncConfiguration } from '../models/syncConfiguration'; +import { SyncConfiguration } from "../models/syncConfiguration"; -import { GroupEntry } from '../models/groupEntry'; -import { UserEntry } from '../models/userEntry'; +import { GroupEntry } from "../models/groupEntry"; +import { UserEntry } from "../models/userEntry"; export abstract class BaseDirectoryService { - protected createDirectoryQuery(filter: string) { - if (filter == null || filter === '') { - return null; - } - - const mainParts = filter.split('|'); - if (mainParts.length < 2 || mainParts[1] == null || mainParts[1].trim() === '') { - return null; - } - - return mainParts[1].trim(); + protected createDirectoryQuery(filter: string) { + if (filter == null || filter === "") { + return null; } - protected createCustomSet(filter: string): [boolean, Set] { - if (filter == null || filter === '') { - return null; - } - - const mainParts = filter.split('|'); - if (mainParts.length < 1 || mainParts[0] == null || mainParts[0].trim() === '') { - return null; - } - - const parts = mainParts[0].split(':'); - if (parts.length !== 2) { - return null; - } - - const keyword = parts[0].trim().toLowerCase(); - let exclude = true; - if (keyword === 'include') { - exclude = false; - } else if (keyword === 'exclude') { - exclude = true; - } else { - return null; - } - - const set = new Set(); - const pieces = parts[1].split(','); - for (const p of pieces) { - set.add(p.trim().toLowerCase()); - } - - return [exclude, set]; + const mainParts = filter.split("|"); + if (mainParts.length < 2 || mainParts[1] == null || mainParts[1].trim() === "") { + return null; } - protected filterOutResult(setFilter: [boolean, Set], result: string) { - if (setFilter != null) { - const cleanResult = result != null ? result.trim().toLowerCase() : '--'; - const excluded = setFilter[0]; - const set = setFilter[1]; + return mainParts[1].trim(); + } - if (excluded && set.has(cleanResult)) { - return true; - } else if (!excluded && !set.has(cleanResult)) { - return true; - } - } - - return false; + protected createCustomSet(filter: string): [boolean, Set] { + if (filter == null || filter === "") { + return null; } - protected filterUsersFromGroupsSet(users: UserEntry[], groups: GroupEntry[], - setFilter: [boolean, Set], syncConfig: SyncConfiguration): UserEntry[] { - if (setFilter == null || users == null) { - return users; - } - - return users.filter(u => { - if (u.deleted) { - return true; - } - if (u.disabled && syncConfig.removeDisabled) { - return true; - } - - return groups.filter(g => g.userMemberExternalIds.has(u.externalId)).length > 0; - }); + const mainParts = filter.split("|"); + if (mainParts.length < 1 || mainParts[0] == null || mainParts[0].trim() === "") { + return null; } - protected forceGroup(force: boolean, users: UserEntry[]): boolean { - return force || (users != null && users.filter(u => !u.deleted && !u.disabled).length > 0); + const parts = mainParts[0].split(":"); + if (parts.length !== 2) { + return null; } + + const keyword = parts[0].trim().toLowerCase(); + let exclude = true; + if (keyword === "include") { + exclude = false; + } else if (keyword === "exclude") { + exclude = true; + } else { + return null; + } + + const set = new Set(); + const pieces = parts[1].split(","); + for (const p of pieces) { + set.add(p.trim().toLowerCase()); + } + + return [exclude, set]; + } + + protected filterOutResult(setFilter: [boolean, Set], result: string) { + if (setFilter != null) { + const cleanResult = result != null ? result.trim().toLowerCase() : "--"; + const excluded = setFilter[0]; + const set = setFilter[1]; + + if (excluded && set.has(cleanResult)) { + return true; + } else if (!excluded && !set.has(cleanResult)) { + return true; + } + } + + return false; + } + + protected filterUsersFromGroupsSet( + users: UserEntry[], + groups: GroupEntry[], + setFilter: [boolean, Set], + syncConfig: SyncConfiguration + ): UserEntry[] { + if (setFilter == null || users == null) { + return users; + } + + return users.filter((u) => { + if (u.deleted) { + return true; + } + if (u.disabled && syncConfig.removeDisabled) { + return true; + } + + return groups.filter((g) => g.userMemberExternalIds.has(u.externalId)).length > 0; + }); + } + + protected forceGroup(force: boolean, users: UserEntry[]): boolean { + return force || (users != null && users.filter((u) => !u.deleted && !u.disabled).length > 0); + } } diff --git a/src/services/configuration.service.ts b/src/services/configuration.service.ts index 0d4436e3..73ce5478 100644 --- a/src/services/configuration.service.ts +++ b/src/services/configuration.service.ts @@ -1,229 +1,238 @@ -import { DirectoryType } from '../enums/directoryType'; +import { DirectoryType } from "../enums/directoryType"; -import { StorageService } from 'jslib-common/abstractions/storage.service'; -import { AzureConfiguration } from '../models/azureConfiguration'; -import { GSuiteConfiguration } from '../models/gsuiteConfiguration'; -import { LdapConfiguration } from '../models/ldapConfiguration'; -import { OktaConfiguration } from '../models/oktaConfiguration'; -import { OneLoginConfiguration } from '../models/oneLoginConfiguration'; -import { SyncConfiguration } from '../models/syncConfiguration'; +import { StorageService } from "jslib-common/abstractions/storage.service"; +import { AzureConfiguration } from "../models/azureConfiguration"; +import { GSuiteConfiguration } from "../models/gsuiteConfiguration"; +import { LdapConfiguration } from "../models/ldapConfiguration"; +import { OktaConfiguration } from "../models/oktaConfiguration"; +import { OneLoginConfiguration } from "../models/oneLoginConfiguration"; +import { SyncConfiguration } from "../models/syncConfiguration"; -const StoredSecurely = '[STORED SECURELY]'; +const StoredSecurely = "[STORED SECURELY]"; const Keys = { - ldap: 'ldapPassword', - gsuite: 'gsuitePrivateKey', - azure: 'azureKey', - okta: 'oktaToken', - oneLogin: 'oneLoginClientSecret', - directoryConfigPrefix: 'directoryConfig_', - sync: 'syncConfig', - directoryType: 'directoryType', - userDelta: 'userDeltaToken', - groupDelta: 'groupDeltaToken', - lastUserSync: 'lastUserSync', - lastGroupSync: 'lastGroupSync', - lastSyncHash: 'lastSyncHash', - organizationId: 'organizationId', + ldap: "ldapPassword", + gsuite: "gsuitePrivateKey", + azure: "azureKey", + okta: "oktaToken", + oneLogin: "oneLoginClientSecret", + directoryConfigPrefix: "directoryConfig_", + sync: "syncConfig", + directoryType: "directoryType", + userDelta: "userDeltaToken", + groupDelta: "groupDeltaToken", + lastUserSync: "lastUserSync", + lastGroupSync: "lastGroupSync", + lastSyncHash: "lastSyncHash", + organizationId: "organizationId", }; export class ConfigurationService { - constructor(private storageService: StorageService, private secureStorageService: StorageService, - private useSecureStorageForSecrets = true) { } + constructor( + private storageService: StorageService, + private secureStorageService: StorageService, + private useSecureStorageForSecrets = true + ) {} - async getDirectory(type: DirectoryType): Promise { - const config = await this.storageService.get(Keys.directoryConfigPrefix + type); - if (config == null) { - return config; - } - - if (this.useSecureStorageForSecrets) { - switch (type) { - case DirectoryType.Ldap: - (config as any).password = await this.secureStorageService.get(Keys.ldap); - break; - case DirectoryType.AzureActiveDirectory: - (config as any).key = await this.secureStorageService.get(Keys.azure); - break; - case DirectoryType.Okta: - (config as any).token = await this.secureStorageService.get(Keys.okta); - break; - case DirectoryType.GSuite: - (config as any).privateKey = await this.secureStorageService.get(Keys.gsuite); - break; - case DirectoryType.OneLogin: - (config as any).clientSecret = await this.secureStorageService.get(Keys.oneLogin); - break; - } - } - return config; + async getDirectory(type: DirectoryType): Promise { + const config = await this.storageService.get(Keys.directoryConfigPrefix + type); + if (config == null) { + return config; } - async saveDirectory(type: DirectoryType, - config: LdapConfiguration | GSuiteConfiguration | AzureConfiguration | OktaConfiguration | - OneLoginConfiguration): Promise { - const savedConfig: any = Object.assign({}, config); - if (this.useSecureStorageForSecrets) { - switch (type) { - case DirectoryType.Ldap: - if (savedConfig.password == null) { - await this.secureStorageService.remove(Keys.ldap); - } else { - await this.secureStorageService.save(Keys.ldap, savedConfig.password); - savedConfig.password = StoredSecurely; - } - break; - case DirectoryType.AzureActiveDirectory: - if (savedConfig.key == null) { - await this.secureStorageService.remove(Keys.azure); - } else { - await this.secureStorageService.save(Keys.azure, savedConfig.key); - savedConfig.key = StoredSecurely; - } - break; - case DirectoryType.Okta: - if (savedConfig.token == null) { - await this.secureStorageService.remove(Keys.okta); - } else { - await this.secureStorageService.save(Keys.okta, savedConfig.token); - savedConfig.token = StoredSecurely; - } - break; - case DirectoryType.GSuite: - if (savedConfig.privateKey == null) { - await this.secureStorageService.remove(Keys.gsuite); - } else { - (config as GSuiteConfiguration).privateKey = savedConfig.privateKey = - savedConfig.privateKey.replace(/\\n/g, '\n'); - await this.secureStorageService.save(Keys.gsuite, savedConfig.privateKey); - savedConfig.privateKey = StoredSecurely; - } - break; - case DirectoryType.OneLogin: - if (savedConfig.clientSecret == null) { - await this.secureStorageService.remove(Keys.oneLogin); - } else { - await this.secureStorageService.save(Keys.oneLogin, savedConfig.clientSecret); - savedConfig.clientSecret = StoredSecurely; - } - break; - } - } - await this.storageService.save(Keys.directoryConfigPrefix + type, savedConfig); + if (this.useSecureStorageForSecrets) { + switch (type) { + case DirectoryType.Ldap: + (config as any).password = await this.secureStorageService.get(Keys.ldap); + break; + case DirectoryType.AzureActiveDirectory: + (config as any).key = await this.secureStorageService.get(Keys.azure); + break; + case DirectoryType.Okta: + (config as any).token = await this.secureStorageService.get(Keys.okta); + break; + case DirectoryType.GSuite: + (config as any).privateKey = await this.secureStorageService.get(Keys.gsuite); + break; + case DirectoryType.OneLogin: + (config as any).clientSecret = await this.secureStorageService.get(Keys.oneLogin); + break; + } + } + return config; + } + + async saveDirectory( + type: DirectoryType, + config: + | LdapConfiguration + | GSuiteConfiguration + | AzureConfiguration + | OktaConfiguration + | OneLoginConfiguration + ): Promise { + const savedConfig: any = Object.assign({}, config); + if (this.useSecureStorageForSecrets) { + switch (type) { + case DirectoryType.Ldap: + if (savedConfig.password == null) { + await this.secureStorageService.remove(Keys.ldap); + } else { + await this.secureStorageService.save(Keys.ldap, savedConfig.password); + savedConfig.password = StoredSecurely; + } + break; + case DirectoryType.AzureActiveDirectory: + if (savedConfig.key == null) { + await this.secureStorageService.remove(Keys.azure); + } else { + await this.secureStorageService.save(Keys.azure, savedConfig.key); + savedConfig.key = StoredSecurely; + } + break; + case DirectoryType.Okta: + if (savedConfig.token == null) { + await this.secureStorageService.remove(Keys.okta); + } else { + await this.secureStorageService.save(Keys.okta, savedConfig.token); + savedConfig.token = StoredSecurely; + } + break; + case DirectoryType.GSuite: + if (savedConfig.privateKey == null) { + await this.secureStorageService.remove(Keys.gsuite); + } else { + (config as GSuiteConfiguration).privateKey = savedConfig.privateKey = + savedConfig.privateKey.replace(/\\n/g, "\n"); + await this.secureStorageService.save(Keys.gsuite, savedConfig.privateKey); + savedConfig.privateKey = StoredSecurely; + } + break; + case DirectoryType.OneLogin: + if (savedConfig.clientSecret == null) { + await this.secureStorageService.remove(Keys.oneLogin); + } else { + await this.secureStorageService.save(Keys.oneLogin, savedConfig.clientSecret); + savedConfig.clientSecret = StoredSecurely; + } + break; + } + } + await this.storageService.save(Keys.directoryConfigPrefix + type, savedConfig); + } + + getSync(): Promise { + return this.storageService.get(Keys.sync); + } + + saveSync(config: SyncConfiguration) { + return this.storageService.save(Keys.sync, config); + } + + getDirectoryType(): Promise { + return this.storageService.get(Keys.directoryType); + } + + async saveDirectoryType(type: DirectoryType) { + const currentType = await this.getDirectoryType(); + if (type !== currentType) { + await this.clearStatefulSettings(); } - getSync(): Promise { - return this.storageService.get(Keys.sync); + return this.storageService.save(Keys.directoryType, type); + } + + getUserDeltaToken(): Promise { + return this.storageService.get(Keys.userDelta); + } + + saveUserDeltaToken(token: string) { + if (token == null) { + return this.storageService.remove(Keys.userDelta); + } else { + return this.storageService.save(Keys.userDelta, token); + } + } + + getGroupDeltaToken(): Promise { + return this.storageService.get(Keys.groupDelta); + } + + saveGroupDeltaToken(token: string) { + if (token == null) { + return this.storageService.remove(Keys.groupDelta); + } else { + return this.storageService.save(Keys.groupDelta, token); + } + } + + async getLastUserSyncDate(): Promise { + const dateString = await this.storageService.get(Keys.lastUserSync); + if (dateString == null) { + return null; + } + return new Date(dateString); + } + + saveLastUserSyncDate(date: Date) { + if (date == null) { + return this.storageService.remove(Keys.lastUserSync); + } else { + return this.storageService.save(Keys.lastUserSync, date); + } + } + + async getLastGroupSyncDate(): Promise { + const dateString = await this.storageService.get(Keys.lastGroupSync); + if (dateString == null) { + return null; + } + return new Date(dateString); + } + + saveLastGroupSyncDate(date: Date) { + if (date == null) { + return this.storageService.remove(Keys.lastGroupSync); + } else { + return this.storageService.save(Keys.lastGroupSync, date); + } + } + + getLastSyncHash(): Promise { + return this.storageService.get(Keys.lastSyncHash); + } + + saveLastSyncHash(hash: string) { + if (hash == null) { + return this.storageService.remove(Keys.lastSyncHash); + } else { + return this.storageService.save(Keys.lastSyncHash, hash); + } + } + + getOrganizationId(): Promise { + return this.storageService.get(Keys.organizationId); + } + + async saveOrganizationId(id: string) { + const currentId = await this.getOrganizationId(); + if (currentId !== id) { + await this.clearStatefulSettings(); } - saveSync(config: SyncConfiguration) { - return this.storageService.save(Keys.sync, config); + if (id == null) { + return this.storageService.remove(Keys.organizationId); + } else { + return this.storageService.save(Keys.organizationId, id); } + } - getDirectoryType(): Promise { - return this.storageService.get(Keys.directoryType); - } - - async saveDirectoryType(type: DirectoryType) { - const currentType = await this.getDirectoryType(); - if (type !== currentType) { - await this.clearStatefulSettings(); - } - - return this.storageService.save(Keys.directoryType, type); - } - - getUserDeltaToken(): Promise { - return this.storageService.get(Keys.userDelta); - } - - saveUserDeltaToken(token: string) { - if (token == null) { - return this.storageService.remove(Keys.userDelta); - } else { - return this.storageService.save(Keys.userDelta, token); - } - } - - getGroupDeltaToken(): Promise { - return this.storageService.get(Keys.groupDelta); - } - - saveGroupDeltaToken(token: string) { - if (token == null) { - return this.storageService.remove(Keys.groupDelta); - } else { - return this.storageService.save(Keys.groupDelta, token); - } - } - - async getLastUserSyncDate(): Promise { - const dateString = await this.storageService.get(Keys.lastUserSync); - if (dateString == null) { - return null; - } - return new Date(dateString); - } - - saveLastUserSyncDate(date: Date) { - if (date == null) { - return this.storageService.remove(Keys.lastUserSync); - } else { - return this.storageService.save(Keys.lastUserSync, date); - } - } - - async getLastGroupSyncDate(): Promise { - const dateString = await this.storageService.get(Keys.lastGroupSync); - if (dateString == null) { - return null; - } - return new Date(dateString); - } - - saveLastGroupSyncDate(date: Date) { - if (date == null) { - return this.storageService.remove(Keys.lastGroupSync); - } else { - return this.storageService.save(Keys.lastGroupSync, date); - } - } - - getLastSyncHash(): Promise { - return this.storageService.get(Keys.lastSyncHash); - } - - saveLastSyncHash(hash: string) { - if (hash == null) { - return this.storageService.remove(Keys.lastSyncHash); - } else { - return this.storageService.save(Keys.lastSyncHash, hash); - } - } - - getOrganizationId(): Promise { - return this.storageService.get(Keys.organizationId); - } - - async saveOrganizationId(id: string) { - const currentId = await this.getOrganizationId(); - if (currentId !== id) { - await this.clearStatefulSettings(); - } - - if (id == null) { - return this.storageService.remove(Keys.organizationId); - } else { - return this.storageService.save(Keys.organizationId, id); - } - } - - async clearStatefulSettings(hashToo = false) { - await this.saveUserDeltaToken(null); - await this.saveGroupDeltaToken(null); - await this.saveLastGroupSyncDate(null); - await this.saveLastUserSyncDate(null); - if (hashToo) { - await this.saveLastSyncHash(null); - } + async clearStatefulSettings(hashToo = false) { + await this.saveUserDeltaToken(null); + await this.saveGroupDeltaToken(null); + await this.saveLastGroupSyncDate(null); + await this.saveLastUserSyncDate(null); + if (hashToo) { + await this.saveLastSyncHash(null); } + } } diff --git a/src/services/directory.service.ts b/src/services/directory.service.ts index 323643ed..e13f8732 100644 --- a/src/services/directory.service.ts +++ b/src/services/directory.service.ts @@ -1,6 +1,6 @@ -import { GroupEntry } from '../models/groupEntry'; -import { UserEntry } from '../models/userEntry'; +import { GroupEntry } from "../models/groupEntry"; +import { UserEntry } from "../models/userEntry"; export interface IDirectoryService { - getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]>; + getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]>; } diff --git a/src/services/gsuite-directory.service.ts b/src/services/gsuite-directory.service.ts index e6260400..ec053618 100644 --- a/src/services/gsuite-directory.service.ts +++ b/src/services/gsuite-directory.service.ts @@ -1,248 +1,260 @@ -import { JWT } from 'google-auth-library'; -import { - admin_directory_v1, - google, -} from 'googleapis'; +import { JWT } from "google-auth-library"; +import { admin_directory_v1, google } from "googleapis"; -import { DirectoryType } from '../enums/directoryType'; +import { DirectoryType } from "../enums/directoryType"; -import { GroupEntry } from '../models/groupEntry'; -import { GSuiteConfiguration } from '../models/gsuiteConfiguration'; -import { SyncConfiguration } from '../models/syncConfiguration'; -import { UserEntry } from '../models/userEntry'; +import { GroupEntry } from "../models/groupEntry"; +import { GSuiteConfiguration } from "../models/gsuiteConfiguration"; +import { SyncConfiguration } from "../models/syncConfiguration"; +import { UserEntry } from "../models/userEntry"; -import { BaseDirectoryService } from './baseDirectory.service'; -import { ConfigurationService } from './configuration.service'; -import { IDirectoryService } from './directory.service'; +import { BaseDirectoryService } from "./baseDirectory.service"; +import { ConfigurationService } from "./configuration.service"; +import { IDirectoryService } from "./directory.service"; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { LogService } from 'jslib-common/abstractions/log.service'; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { LogService } from "jslib-common/abstractions/log.service"; export class GSuiteDirectoryService extends BaseDirectoryService implements IDirectoryService { - private client: JWT; - private service: admin_directory_v1.Admin; - private authParams: any; - private dirConfig: GSuiteConfiguration; - private syncConfig: SyncConfiguration; + private client: JWT; + private service: admin_directory_v1.Admin; + private authParams: any; + private dirConfig: GSuiteConfiguration; + private syncConfig: SyncConfiguration; - constructor(private configurationService: ConfigurationService, private logService: LogService, - private i18nService: I18nService) { - super(); - this.service = google.admin('directory_v1'); + constructor( + private configurationService: ConfigurationService, + private logService: LogService, + private i18nService: I18nService + ) { + super(); + this.service = google.admin("directory_v1"); + } + + async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> { + const type = await this.configurationService.getDirectoryType(); + if (type !== DirectoryType.GSuite) { + return; } - async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> { - const type = await this.configurationService.getDirectoryType(); - if (type !== DirectoryType.GSuite) { - return; - } - - this.dirConfig = await this.configurationService.getDirectory(DirectoryType.GSuite); - if (this.dirConfig == null) { - return; - } - - this.syncConfig = await this.configurationService.getSync(); - if (this.syncConfig == null) { - return; - } - - await this.auth(); - - let users: UserEntry[] = []; - if (this.syncConfig.users) { - users = await this.getUsers(); - } - - let groups: GroupEntry[]; - if (this.syncConfig.groups) { - const setFilter = this.createCustomSet(this.syncConfig.groupFilter); - groups = await this.getGroups(setFilter, users); - users = this.filterUsersFromGroupsSet(users, groups, setFilter, this.syncConfig); - } - - return [groups, users]; + this.dirConfig = await this.configurationService.getDirectory( + DirectoryType.GSuite + ); + if (this.dirConfig == null) { + return; } - private async getUsers(): Promise { - const entries: UserEntry[] = []; - const query = this.createDirectoryQuery(this.syncConfig.userFilter); - let nextPageToken: string = null; - - const filter = this.createCustomSet(this.syncConfig.userFilter); - while (true) { - this.logService.info('Querying users - nextPageToken:' + nextPageToken); - const p = Object.assign({ query: query, pageToken: nextPageToken }, this.authParams); - const res = await this.service.users.list(p); - if (res.status !== 200) { - throw new Error('User list API failed: ' + res.statusText); - } - - nextPageToken = res.data.nextPageToken; - if (res.data.users != null) { - for (const user of res.data.users) { - if (this.filterOutResult(filter, user.primaryEmail)) { - continue; - } - const entry = this.buildUser(user, false); - if (entry != null) { - entries.push(entry); - } - } - } - - if (nextPageToken == null) { - break; - } - } - - nextPageToken = null; - while (true) { - this.logService.info('Querying deleted users - nextPageToken:' + nextPageToken); - const p = Object.assign({ showDeleted: true, query: query, pageToken: nextPageToken }, this.authParams); - const delRes = await this.service.users.list(p); - if (delRes.status !== 200) { - throw new Error('Deleted user list API failed: ' + delRes.statusText); - } - - nextPageToken = delRes.data.nextPageToken; - if (delRes.data.users != null) { - for (const user of delRes.data.users) { - if (this.filterOutResult(filter, user.primaryEmail)) { - continue; - } - const entry = this.buildUser(user, true); - if (entry != null) { - entries.push(entry); - } - } - } - - if (nextPageToken == null) { - break; - } - } - - return entries; + this.syncConfig = await this.configurationService.getSync(); + if (this.syncConfig == null) { + return; } - private buildUser(user: admin_directory_v1.Schema$User, deleted: boolean) { - if ((user.emails == null || user.emails === '') && !deleted) { - return null; - } + await this.auth(); - const entry = new UserEntry(); - entry.referenceId = user.id; - entry.externalId = user.id; - entry.email = user.primaryEmail != null ? user.primaryEmail.trim().toLowerCase() : null; - entry.disabled = user.suspended || false; - entry.deleted = deleted; + let users: UserEntry[] = []; + if (this.syncConfig.users) { + users = await this.getUsers(); + } + + let groups: GroupEntry[]; + if (this.syncConfig.groups) { + const setFilter = this.createCustomSet(this.syncConfig.groupFilter); + groups = await this.getGroups(setFilter, users); + users = this.filterUsersFromGroupsSet(users, groups, setFilter, this.syncConfig); + } + + return [groups, users]; + } + + private async getUsers(): Promise { + const entries: UserEntry[] = []; + const query = this.createDirectoryQuery(this.syncConfig.userFilter); + let nextPageToken: string = null; + + const filter = this.createCustomSet(this.syncConfig.userFilter); + while (true) { + this.logService.info("Querying users - nextPageToken:" + nextPageToken); + const p = Object.assign({ query: query, pageToken: nextPageToken }, this.authParams); + const res = await this.service.users.list(p); + if (res.status !== 200) { + throw new Error("User list API failed: " + res.statusText); + } + + nextPageToken = res.data.nextPageToken; + if (res.data.users != null) { + for (const user of res.data.users) { + if (this.filterOutResult(filter, user.primaryEmail)) { + continue; + } + const entry = this.buildUser(user, false); + if (entry != null) { + entries.push(entry); + } + } + } + + if (nextPageToken == null) { + break; + } + } + + nextPageToken = null; + while (true) { + this.logService.info("Querying deleted users - nextPageToken:" + nextPageToken); + const p = Object.assign( + { showDeleted: true, query: query, pageToken: nextPageToken }, + this.authParams + ); + const delRes = await this.service.users.list(p); + if (delRes.status !== 200) { + throw new Error("Deleted user list API failed: " + delRes.statusText); + } + + nextPageToken = delRes.data.nextPageToken; + if (delRes.data.users != null) { + for (const user of delRes.data.users) { + if (this.filterOutResult(filter, user.primaryEmail)) { + continue; + } + const entry = this.buildUser(user, true); + if (entry != null) { + entries.push(entry); + } + } + } + + if (nextPageToken == null) { + break; + } + } + + return entries; + } + + private buildUser(user: admin_directory_v1.Schema$User, deleted: boolean) { + if ((user.emails == null || user.emails === "") && !deleted) { + return null; + } + + const entry = new UserEntry(); + entry.referenceId = user.id; + entry.externalId = user.id; + entry.email = user.primaryEmail != null ? user.primaryEmail.trim().toLowerCase() : null; + entry.disabled = user.suspended || false; + entry.deleted = deleted; + return entry; + } + + private async getGroups( + setFilter: [boolean, Set], + users: UserEntry[] + ): Promise { + const entries: GroupEntry[] = []; + let nextPageToken: string = null; + + while (true) { + this.logService.info("Querying groups - nextPageToken:" + nextPageToken); + const p = Object.assign({ pageToken: nextPageToken }, this.authParams); + const res = await this.service.groups.list(p); + if (res.status !== 200) { + throw new Error("Group list API failed: " + res.statusText); + } + + nextPageToken = res.data.nextPageToken; + if (res.data.groups != null) { + for (const group of res.data.groups) { + if (!this.filterOutResult(setFilter, group.name)) { + const entry = await this.buildGroup(group, users); + entries.push(entry); + } + } + } + + if (nextPageToken == null) { + break; + } + } + + return entries; + } + + private async buildGroup(group: admin_directory_v1.Schema$Group, users: UserEntry[]) { + let nextPageToken: string = null; + + const entry = new GroupEntry(); + entry.referenceId = group.id; + entry.externalId = group.id; + entry.name = group.name; + + while (true) { + const p = Object.assign({ groupKey: group.id, pageToken: nextPageToken }, this.authParams); + const memRes = await this.service.members.list(p); + if (memRes.status !== 200) { + this.logService.warning("Group member list API failed: " + memRes.statusText); return entry; + } + + nextPageToken = memRes.data.nextPageToken; + if (memRes.data.members != null) { + for (const member of memRes.data.members) { + if (member.type == null) { + continue; + } + const type = member.type.toLowerCase(); + if (type === "user") { + if (member.status == null || member.status.toLowerCase() !== "active") { + continue; + } + entry.userMemberExternalIds.add(member.id); + } else if (type === "group") { + entry.groupMemberReferenceIds.add(member.id); + } else if (type === "customer") { + for (const user of users) { + entry.userMemberExternalIds.add(user.externalId); + } + } + } + } + + if (nextPageToken == null) { + break; + } } - private async getGroups(setFilter: [boolean, Set], users: UserEntry[]): Promise { - const entries: GroupEntry[] = []; - let nextPageToken: string = null; + return entry; + } - while (true) { - this.logService.info('Querying groups - nextPageToken:' + nextPageToken); - const p = Object.assign({ pageToken: nextPageToken }, this.authParams); - const res = await this.service.groups.list(p); - if (res.status !== 200) { - throw new Error('Group list API failed: ' + res.statusText); - } - - nextPageToken = res.data.nextPageToken; - if (res.data.groups != null) { - for (const group of res.data.groups) { - if (!this.filterOutResult(setFilter, group.name)) { - const entry = await this.buildGroup(group, users); - entries.push(entry); - } - } - } - - if (nextPageToken == null) { - break; - } - } - - return entries; + private async auth() { + if ( + this.dirConfig.clientEmail == null || + this.dirConfig.privateKey == null || + this.dirConfig.adminUser == null || + this.dirConfig.domain == null + ) { + throw new Error(this.i18nService.t("dirConfigIncomplete")); } - private async buildGroup(group: admin_directory_v1.Schema$Group, users: UserEntry[]) { - let nextPageToken: string = null; + this.client = new google.auth.JWT({ + email: this.dirConfig.clientEmail, + key: this.dirConfig.privateKey != null ? this.dirConfig.privateKey.trimLeft() : null, + subject: this.dirConfig.adminUser, + scopes: [ + "https://www.googleapis.com/auth/admin.directory.user.readonly", + "https://www.googleapis.com/auth/admin.directory.group.readonly", + "https://www.googleapis.com/auth/admin.directory.group.member.readonly", + ], + }); - const entry = new GroupEntry(); - entry.referenceId = group.id; - entry.externalId = group.id; - entry.name = group.name; + await this.client.authorize(); - while (true) { - const p = Object.assign({ groupKey: group.id, pageToken: nextPageToken }, this.authParams); - const memRes = await this.service.members.list(p); - if (memRes.status !== 200) { - this.logService.warning('Group member list API failed: ' + memRes.statusText); - return entry; - } - - nextPageToken = memRes.data.nextPageToken; - if (memRes.data.members != null) { - for (const member of memRes.data.members) { - if (member.type == null) { - continue; - } - const type = member.type.toLowerCase(); - if (type === 'user') { - if (member.status == null || member.status.toLowerCase() !== 'active') { - continue; - } - entry.userMemberExternalIds.add(member.id); - } else if (type === 'group') { - entry.groupMemberReferenceIds.add(member.id); - } else if (type === 'customer') { - for (const user of users) { - entry.userMemberExternalIds.add(user.externalId); - } - } - } - } - - if (nextPageToken == null) { - break; - } - } - - return entry; + this.authParams = { + auth: this.client, + }; + if (this.dirConfig.domain != null && this.dirConfig.domain.trim() !== "") { + this.authParams.domain = this.dirConfig.domain; } - - private async auth() { - if (this.dirConfig.clientEmail == null || this.dirConfig.privateKey == null || - this.dirConfig.adminUser == null || this.dirConfig.domain == null) { - throw new Error(this.i18nService.t('dirConfigIncomplete')); - } - - this.client = new google.auth.JWT({ - email: this.dirConfig.clientEmail, - key: this.dirConfig.privateKey != null ? this.dirConfig.privateKey.trimLeft() : null, - subject: this.dirConfig.adminUser, - scopes: [ - 'https://www.googleapis.com/auth/admin.directory.user.readonly', - 'https://www.googleapis.com/auth/admin.directory.group.readonly', - 'https://www.googleapis.com/auth/admin.directory.group.member.readonly', - ], - }); - - await this.client.authorize(); - - this.authParams = { - auth: this.client, - }; - 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; - } + if (this.dirConfig.customer != null && this.dirConfig.customer.trim() !== "") { + this.authParams.customer = this.dirConfig.customer; } + } } diff --git a/src/services/i18n.service.ts b/src/services/i18n.service.ts index 23f38d31..ed89d73c 100644 --- a/src/services/i18n.service.ts +++ b/src/services/i18n.service.ts @@ -1,15 +1,18 @@ -import * as fs from 'fs'; -import * as path from 'path'; +import * as fs from "fs"; +import * as path from "path"; -import { I18nService as BaseI18nService } from 'jslib-common/services/i18n.service'; +import { I18nService as BaseI18nService } from "jslib-common/services/i18n.service"; export class I18nService extends BaseI18nService { - constructor(systemLanguage: string, localesDirectory: string) { - super(systemLanguage, localesDirectory, (formattedLocale: string) => { - const filePath = path.join(__dirname, this.localesDirectory + '/' + formattedLocale + '/messages.json'); - const localesJson = fs.readFileSync(filePath, 'utf8'); - const locales = JSON.parse(localesJson.replace(/^\uFEFF/, '')); // strip the BOM - return Promise.resolve(locales); - }); - } + constructor(systemLanguage: string, localesDirectory: string) { + super(systemLanguage, localesDirectory, (formattedLocale: string) => { + const filePath = path.join( + __dirname, + this.localesDirectory + "/" + formattedLocale + "/messages.json" + ); + const localesJson = fs.readFileSync(filePath, "utf8"); + const locales = JSON.parse(localesJson.replace(/^\uFEFF/, "")); // strip the BOM + return Promise.resolve(locales); + }); + } } diff --git a/src/services/keytarSecureStorage.service.ts b/src/services/keytarSecureStorage.service.ts index 3149fdc6..faadcde5 100644 --- a/src/services/keytarSecureStorage.service.ts +++ b/src/services/keytarSecureStorage.service.ts @@ -1,29 +1,25 @@ -import { - deletePassword, - getPassword, - setPassword, -} from 'keytar'; +import { deletePassword, getPassword, setPassword } from "keytar"; -import { StorageService } from 'jslib-common/abstractions/storage.service'; +import { StorageService } from "jslib-common/abstractions/storage.service"; export class KeytarSecureStorageService implements StorageService { - constructor(private serviceName: string) { } + constructor(private serviceName: string) {} - get(key: string): Promise { - return getPassword(this.serviceName, key).then(val => { - return JSON.parse(val) as T; - }); - } + get(key: string): Promise { + return getPassword(this.serviceName, key).then((val) => { + return JSON.parse(val) as T; + }); + } - async has(key: string): Promise { - return (await this.get(key)) != null; - } + async has(key: string): Promise { + return (await this.get(key)) != null; + } - save(key: string, obj: any): Promise { - return setPassword(this.serviceName, key, JSON.stringify(obj)); - } + save(key: string, obj: any): Promise { + return setPassword(this.serviceName, key, JSON.stringify(obj)); + } - remove(key: string): Promise { - return deletePassword(this.serviceName, key); - } + remove(key: string): Promise { + return deletePassword(this.serviceName, key); + } } diff --git a/src/services/ldap-directory.service.ts b/src/services/ldap-directory.service.ts index de72e73b..1b1f7638 100644 --- a/src/services/ldap-directory.service.ts +++ b/src/services/ldap-directory.service.ts @@ -1,450 +1,506 @@ -import * as fs from 'fs'; -import * as ldap from 'ldapjs'; +import * as fs from "fs"; +import * as ldap from "ldapjs"; -import { checkServerIdentity, PeerCertificate } from 'tls'; +import { checkServerIdentity, PeerCertificate } from "tls"; -import { DirectoryType } from '../enums/directoryType'; +import { DirectoryType } from "../enums/directoryType"; -import { GroupEntry } from '../models/groupEntry'; -import { LdapConfiguration } from '../models/ldapConfiguration'; -import { SyncConfiguration } from '../models/syncConfiguration'; -import { UserEntry } from '../models/userEntry'; +import { GroupEntry } from "../models/groupEntry"; +import { LdapConfiguration } from "../models/ldapConfiguration"; +import { SyncConfiguration } from "../models/syncConfiguration"; +import { UserEntry } from "../models/userEntry"; -import { ConfigurationService } from './configuration.service'; -import { IDirectoryService } from './directory.service'; +import { ConfigurationService } from "./configuration.service"; +import { IDirectoryService } from "./directory.service"; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { LogService } from 'jslib-common/abstractions/log.service'; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { LogService } from "jslib-common/abstractions/log.service"; -import { Utils } from 'jslib-common/misc/utils'; +import { Utils } from "jslib-common/misc/utils"; const UserControlAccountDisabled = 2; export class LdapDirectoryService implements IDirectoryService { - private client: ldap.Client; - private dirConfig: LdapConfiguration; - private syncConfig: SyncConfiguration; + private client: ldap.Client; + private dirConfig: LdapConfiguration; + private syncConfig: SyncConfiguration; - constructor(private configurationService: ConfigurationService, private logService: LogService, - private i18nService: I18nService) { } + constructor( + private configurationService: ConfigurationService, + private logService: LogService, + private i18nService: I18nService + ) {} - async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> { - const type = await this.configurationService.getDirectoryType(); - if (type !== DirectoryType.Ldap) { - return; - } - - this.dirConfig = await this.configurationService.getDirectory(DirectoryType.Ldap); - if (this.dirConfig == null) { - return; - } - - this.syncConfig = await this.configurationService.getSync(); - if (this.syncConfig == null) { - return; - } - - await this.bind(); - - let users: UserEntry[]; - if (this.syncConfig.users) { - users = await this.getUsers(force); - } - - let groups: GroupEntry[]; - if (this.syncConfig.groups) { - let groupForce = force; - if (!groupForce && users != null) { - const activeUsers = users.filter(u => !u.deleted && !u.disabled); - groupForce = activeUsers.length > 0; - } - groups = await this.getGroups(groupForce); - } - - await this.unbind(); - return [groups, users]; + async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> { + const type = await this.configurationService.getDirectoryType(); + if (type !== DirectoryType.Ldap) { + return; } - private async getUsers(force: boolean): Promise { - const lastSync = await this.configurationService.getLastUserSyncDate(); - let filter = this.buildBaseFilter(this.syncConfig.userObjectClass, this.syncConfig.userFilter); - filter = this.buildRevisionFilter(filter, force, lastSync); - - const path = this.makeSearchPath(this.syncConfig.userPath); - this.logService.info('User search: ' + path + ' => ' + filter); - - const regularUsers = await this.search(path, filter, (se: any) => this.buildUser(se, false)); - if (!this.dirConfig.ad) { - return regularUsers; - } - - try { - let deletedFilter = this.buildBaseFilter(this.syncConfig.userObjectClass, '(isDeleted=TRUE)'); - deletedFilter = this.buildRevisionFilter(deletedFilter, force, lastSync); - - const deletedPath = this.makeSearchPath('CN=Deleted Objects'); - this.logService.info('Deleted user search: ' + deletedPath + ' => ' + deletedFilter); - - const delControl = new (ldap as any).Control({ type: '1.2.840.113556.1.4.417', criticality: true }); - const deletedUsers = await this.search(deletedPath, deletedFilter, - (se: any) => this.buildUser(se, true), [delControl]); - return regularUsers.concat(deletedUsers); - } catch (e) { - this.logService.warning('Cannot query deleted users.'); - return regularUsers; - } + this.dirConfig = await this.configurationService.getDirectory( + DirectoryType.Ldap + ); + if (this.dirConfig == null) { + return; } - private buildUser(searchEntry: any, deleted: boolean): UserEntry { - const user = new UserEntry(); - user.referenceId = searchEntry.objectName; - user.deleted = deleted; - - if (user.referenceId == null) { - return null; - } - - user.externalId = this.getExternalId(searchEntry, user.referenceId); - user.disabled = this.entryDisabled(searchEntry); - user.email = this.getAttr(searchEntry, this.syncConfig.userEmailAttribute); - if (user.email == null && this.syncConfig.useEmailPrefixSuffix && - this.syncConfig.emailPrefixAttribute != null && this.syncConfig.emailSuffix != null) { - const prefixAttr = this.getAttr(searchEntry, this.syncConfig.emailPrefixAttribute); - if (prefixAttr != null) { - user.email = prefixAttr + this.syncConfig.emailSuffix; - } - } - - if (user.email != null) { - user.email = user.email.trim().toLowerCase(); - } - - if (!user.deleted && (user.email == null || user.email.trim() === '')) { - return null; - } - - return user; + this.syncConfig = await this.configurationService.getSync(); + if (this.syncConfig == null) { + return; } - private async getGroups(force: boolean): Promise { - const entries: GroupEntry[] = []; + await this.bind(); - const lastSync = await this.configurationService.getLastUserSyncDate(); - const originalFilter = this.buildBaseFilter(this.syncConfig.groupObjectClass, this.syncConfig.groupFilter); - let filter = originalFilter; - const revisionFilter = this.buildRevisionFilter(filter, force, lastSync); - const searchSinceRevision = filter !== revisionFilter; - filter = revisionFilter; + let users: UserEntry[]; + if (this.syncConfig.users) { + users = await this.getUsers(force); + } - const path = this.makeSearchPath(this.syncConfig.groupPath); - this.logService.info('Group search: ' + path + ' => ' + filter); + let groups: GroupEntry[]; + if (this.syncConfig.groups) { + let groupForce = force; + if (!groupForce && users != null) { + const activeUsers = users.filter((u) => !u.deleted && !u.disabled); + groupForce = activeUsers.length > 0; + } + groups = await this.getGroups(groupForce); + } - let groupSearchEntries: any[] = []; - const initialSearchGroupIds = await this.search(path, filter, (se: any) => { - groupSearchEntries.push(se); - return se.objectName; + await this.unbind(); + return [groups, users]; + } + + private async getUsers(force: boolean): Promise { + const lastSync = await this.configurationService.getLastUserSyncDate(); + let filter = this.buildBaseFilter(this.syncConfig.userObjectClass, this.syncConfig.userFilter); + filter = this.buildRevisionFilter(filter, force, lastSync); + + const path = this.makeSearchPath(this.syncConfig.userPath); + this.logService.info("User search: " + path + " => " + filter); + + const regularUsers = await this.search(path, filter, (se: any) => + this.buildUser(se, false) + ); + if (!this.dirConfig.ad) { + return regularUsers; + } + + try { + let deletedFilter = this.buildBaseFilter(this.syncConfig.userObjectClass, "(isDeleted=TRUE)"); + deletedFilter = this.buildRevisionFilter(deletedFilter, force, lastSync); + + const deletedPath = this.makeSearchPath("CN=Deleted Objects"); + this.logService.info("Deleted user search: " + deletedPath + " => " + deletedFilter); + + const delControl = new (ldap as any).Control({ + type: "1.2.840.113556.1.4.417", + criticality: true, + }); + const deletedUsers = await this.search( + deletedPath, + deletedFilter, + (se: any) => this.buildUser(se, true), + [delControl] + ); + return regularUsers.concat(deletedUsers); + } catch (e) { + this.logService.warning("Cannot query deleted users."); + return regularUsers; + } + } + + private buildUser(searchEntry: any, deleted: boolean): UserEntry { + const user = new UserEntry(); + user.referenceId = searchEntry.objectName; + user.deleted = deleted; + + if (user.referenceId == null) { + return null; + } + + user.externalId = this.getExternalId(searchEntry, user.referenceId); + user.disabled = this.entryDisabled(searchEntry); + user.email = this.getAttr(searchEntry, this.syncConfig.userEmailAttribute); + if ( + user.email == null && + this.syncConfig.useEmailPrefixSuffix && + this.syncConfig.emailPrefixAttribute != null && + this.syncConfig.emailSuffix != null + ) { + const prefixAttr = this.getAttr(searchEntry, this.syncConfig.emailPrefixAttribute); + if (prefixAttr != null) { + user.email = prefixAttr + this.syncConfig.emailSuffix; + } + } + + if (user.email != null) { + user.email = user.email.trim().toLowerCase(); + } + + if (!user.deleted && (user.email == null || user.email.trim() === "")) { + return null; + } + + return user; + } + + private async getGroups(force: boolean): Promise { + const entries: GroupEntry[] = []; + + const lastSync = await this.configurationService.getLastUserSyncDate(); + const originalFilter = this.buildBaseFilter( + this.syncConfig.groupObjectClass, + this.syncConfig.groupFilter + ); + let filter = originalFilter; + const revisionFilter = this.buildRevisionFilter(filter, force, lastSync); + const searchSinceRevision = filter !== revisionFilter; + filter = revisionFilter; + + const path = this.makeSearchPath(this.syncConfig.groupPath); + this.logService.info("Group search: " + path + " => " + filter); + + let groupSearchEntries: any[] = []; + const initialSearchGroupIds = await this.search(path, filter, (se: any) => { + groupSearchEntries.push(se); + return se.objectName; + }); + + if (searchSinceRevision && initialSearchGroupIds.length === 0) { + return []; + } else if (searchSinceRevision) { + groupSearchEntries = await this.search(path, originalFilter, (se: any) => se); + } + + const userFilter = this.buildBaseFilter( + this.syncConfig.userObjectClass, + this.syncConfig.userFilter + ); + const userPath = this.makeSearchPath(this.syncConfig.userPath); + const userIdMap = new Map(); + await this.search(userPath, userFilter, (se: any) => { + userIdMap.set(se.objectName, this.getExternalId(se, se.objectName)); + return se; + }); + + for (const se of groupSearchEntries) { + const group = this.buildGroup(se, userIdMap); + if (group != null) { + entries.push(group); + } + } + + return entries; + } + + private buildGroup(searchEntry: any, userMap: Map) { + const group = new GroupEntry(); + group.referenceId = searchEntry.objectName; + if (group.referenceId == null) { + return null; + } + + group.externalId = this.getExternalId(searchEntry, group.referenceId); + + group.name = this.getAttr(searchEntry, this.syncConfig.groupNameAttribute); + if (group.name == null) { + group.name = this.getAttr(searchEntry, "cn"); + } + + if (group.name == null) { + return null; + } + + const members = this.getAttrVals(searchEntry, this.syncConfig.memberAttribute); + if (members != null) { + for (const memDn of members) { + if (userMap.has(memDn) && !group.userMemberExternalIds.has(userMap.get(memDn))) { + group.userMemberExternalIds.add(userMap.get(memDn)); + } else if (!group.groupMemberReferenceIds.has(memDn)) { + group.groupMemberReferenceIds.add(memDn); + } + } + } + + return group; + } + + private getExternalId(searchEntry: any, referenceId: string) { + const attrObj = this.getAttrObj(searchEntry, "objectGUID"); + if (attrObj != null && attrObj._vals != null && attrObj._vals.length > 0) { + return this.bufToGuid(attrObj._vals[0]); + } else { + return referenceId; + } + } + + private buildBaseFilter(objectClass: string, subFilter: string): string { + let filter = this.buildObjectClassFilter(objectClass); + if (subFilter != null && subFilter.trim() !== "") { + filter = "(&" + filter + subFilter + ")"; + } + return filter; + } + + private buildObjectClassFilter(objectClass: string): string { + return "(&(objectClass=" + objectClass + "))"; + } + + private buildRevisionFilter(baseFilter: string, force: boolean, lastRevisionDate: Date) { + const revisionAttr = this.syncConfig.revisionDateAttribute; + if (!force && lastRevisionDate != null && revisionAttr != null && revisionAttr.trim() !== "") { + const dateString = lastRevisionDate.toISOString().replace(/[-:T]/g, "").substr(0, 16) + "Z"; + baseFilter = "(&" + baseFilter + "(" + revisionAttr + ">=" + dateString + "))"; + } + + return baseFilter; + } + + private makeSearchPath(pathPrefix: string) { + if (this.dirConfig.rootPath.toLowerCase().indexOf("dc=") === -1) { + return pathPrefix; + } + if (this.dirConfig.rootPath != null && this.dirConfig.rootPath.trim() !== "") { + const trimmedRootPath = this.dirConfig.rootPath.trim().toLowerCase(); + let path = trimmedRootPath.substr(trimmedRootPath.indexOf("dc=")); + if (pathPrefix != null && pathPrefix.trim() !== "") { + path = pathPrefix.trim() + "," + path; + } + return path; + } + + return null; + } + + private getAttrObj(searchEntry: any, attr: string): any { + if (searchEntry == null || searchEntry.attributes == null) { + return null; + } + + const attrs = searchEntry.attributes.filter((a: any) => a.type === attr); + if ( + attrs == null || + attrs.length === 0 || + attrs[0].vals == null || + attrs[0].vals.length === 0 + ) { + return null; + } + + return attrs[0]; + } + + private getAttrVals(searchEntry: any, attr: string): string[] { + const obj = this.getAttrObj(searchEntry, attr); + if (obj == null) { + return null; + } + return obj.vals; + } + + private getAttr(searchEntry: any, attr: string): string { + const vals = this.getAttrVals(searchEntry, attr); + if (vals == null) { + return null; + } + return vals[0]; + } + + private entryDisabled(searchEntry: any): boolean { + const c = this.getAttr(searchEntry, "userAccountControl"); + if (c != null) { + try { + const control = parseInt(c, null); + // tslint:disable-next-line + return (control & UserControlAccountDisabled) === UserControlAccountDisabled; + } catch (e) { + this.logService.error(e); + } + } + + return false; + } + + private async search( + path: string, + filter: string, + processEntry: (searchEntry: any) => T, + controls: ldap.Control[] = [] + ): Promise { + const options: ldap.SearchOptions = { + filter: filter, + scope: "sub", + paged: this.dirConfig.pagedSearch, + }; + const entries: T[] = []; + return new Promise((resolve, reject) => { + this.client.search(path, options, controls, (err, res) => { + if (err != null) { + reject(err); + return; + } + + res.on("error", (resErr) => { + reject(resErr); }); - if (searchSinceRevision && initialSearchGroupIds.length === 0) { - return []; - } else if (searchSinceRevision) { - groupSearchEntries = await this.search(path, originalFilter, (se: any) => se); - } - - const userFilter = this.buildBaseFilter(this.syncConfig.userObjectClass, this.syncConfig.userFilter); - const userPath = this.makeSearchPath(this.syncConfig.userPath); - const userIdMap = new Map(); - await this.search(userPath, userFilter, (se: any) => { - userIdMap.set(se.objectName, this.getExternalId(se, se.objectName)); - return se; + res.on("searchEntry", (entry) => { + const e = processEntry(entry); + if (e != null) { + entries.push(e); + } }); - for (const se of groupSearchEntries) { - const group = this.buildGroup(se, userIdMap); - if (group != null) { - entries.push(group); - } + res.on("end", (result) => { + resolve(entries); + }); + }); + }); + } + + private async bind(): Promise { + return new Promise((resolve, reject) => { + if (this.dirConfig.hostname == null || this.dirConfig.port == null) { + reject(this.i18nService.t("dirConfigIncomplete")); + return; + } + const protocol = "ldap" + (this.dirConfig.ssl && !this.dirConfig.startTls ? "s" : ""); + const url = protocol + "://" + this.dirConfig.hostname + ":" + this.dirConfig.port; + const options: ldap.ClientOptions = { + url: url.trim().toLowerCase(), + }; + + const tlsOptions: any = {}; + if (this.dirConfig.ssl) { + if (this.dirConfig.sslAllowUnauthorized) { + tlsOptions.rejectUnauthorized = !this.dirConfig.sslAllowUnauthorized; } - - return entries; - } - - private buildGroup(searchEntry: any, userMap: Map) { - const group = new GroupEntry(); - group.referenceId = searchEntry.objectName; - if (group.referenceId == null) { - return null; - } - - group.externalId = this.getExternalId(searchEntry, group.referenceId); - - group.name = this.getAttr(searchEntry, this.syncConfig.groupNameAttribute); - if (group.name == null) { - group.name = this.getAttr(searchEntry, 'cn'); - } - - if (group.name == null) { - return null; - } - - const members = this.getAttrVals(searchEntry, this.syncConfig.memberAttribute); - if (members != null) { - for (const memDn of members) { - if (userMap.has(memDn) && !group.userMemberExternalIds.has(userMap.get(memDn))) { - group.userMemberExternalIds.add(userMap.get(memDn)); - } else if (!group.groupMemberReferenceIds.has(memDn)) { - group.groupMemberReferenceIds.add(memDn); - } - } - } - - return group; - } - - private getExternalId(searchEntry: any, referenceId: string) { - const attrObj = this.getAttrObj(searchEntry, 'objectGUID'); - if (attrObj != null && attrObj._vals != null && attrObj._vals.length > 0) { - return this.bufToGuid(attrObj._vals[0]); + if (!this.dirConfig.startTls) { + if ( + this.dirConfig.sslCaPath != null && + this.dirConfig.sslCaPath !== "" && + fs.existsSync(this.dirConfig.sslCaPath) + ) { + tlsOptions.ca = [fs.readFileSync(this.dirConfig.sslCaPath)]; + } + if ( + this.dirConfig.sslCertPath != null && + this.dirConfig.sslCertPath !== "" && + fs.existsSync(this.dirConfig.sslCertPath) + ) { + tlsOptions.cert = fs.readFileSync(this.dirConfig.sslCertPath); + } + if ( + this.dirConfig.sslKeyPath != null && + this.dirConfig.sslKeyPath !== "" && + fs.existsSync(this.dirConfig.sslKeyPath) + ) { + tlsOptions.key = fs.readFileSync(this.dirConfig.sslKeyPath); + } } else { - return referenceId; + if ( + this.dirConfig.tlsCaPath != null && + this.dirConfig.tlsCaPath !== "" && + fs.existsSync(this.dirConfig.tlsCaPath) + ) { + tlsOptions.ca = [fs.readFileSync(this.dirConfig.tlsCaPath)]; + } } - } + } - private buildBaseFilter(objectClass: string, subFilter: string): string { - let filter = this.buildObjectClassFilter(objectClass); - if (subFilter != null && subFilter.trim() !== '') { - filter = '(&' + filter + subFilter + ')'; - } - return filter; - } + tlsOptions.checkServerIdentity = this.checkServerIdentityAltNames; + options.tlsOptions = tlsOptions; - private buildObjectClassFilter(objectClass: string): string { - return '(&(objectClass=' + objectClass + '))'; - } + this.client = ldap.createClient(options); - private buildRevisionFilter(baseFilter: string, force: boolean, lastRevisionDate: Date) { - const revisionAttr = this.syncConfig.revisionDateAttribute; - if (!force && lastRevisionDate != null && revisionAttr != null && revisionAttr.trim() !== '') { - const dateString = lastRevisionDate.toISOString().replace(/[-:T]/g, '').substr(0, 16) + 'Z'; - baseFilter = '(&' + baseFilter + '(' + revisionAttr + '>=' + dateString + '))'; - } + const user = + this.dirConfig.username == null || this.dirConfig.username.trim() === "" + ? null + : this.dirConfig.username; + const pass = + this.dirConfig.password == null || this.dirConfig.password.trim() === "" + ? null + : this.dirConfig.password; - return baseFilter; - } + if (user == null || pass == null) { + reject(this.i18nService.t("usernamePasswordNotConfigured")); + return; + } - private makeSearchPath(pathPrefix: string) { - if (this.dirConfig.rootPath.toLowerCase().indexOf('dc=') === -1) { - return pathPrefix; - } - if (this.dirConfig.rootPath != null && this.dirConfig.rootPath.trim() !== '') { - const trimmedRootPath = this.dirConfig.rootPath.trim().toLowerCase(); - let path = trimmedRootPath.substr(trimmedRootPath.indexOf('dc=')); - if (pathPrefix != null && pathPrefix.trim() !== '') { - path = pathPrefix.trim() + ',' + path; - } - return path; - } - - return null; - } - - private getAttrObj(searchEntry: any, attr: string): any { - if (searchEntry == null || searchEntry.attributes == null) { - return null; - } - - const attrs = searchEntry.attributes.filter((a: any) => a.type === attr); - if (attrs == null || attrs.length === 0 || attrs[0].vals == null || attrs[0].vals.length === 0) { - return null; - } - - return attrs[0]; - } - - private getAttrVals(searchEntry: any, attr: string): string[] { - const obj = this.getAttrObj(searchEntry, attr); - if (obj == null) { - return null; - } - return obj.vals; - } - - private getAttr(searchEntry: any, attr: string): string { - const vals = this.getAttrVals(searchEntry, attr); - if (vals == null) { - return null; - } - return vals[0]; - } - - private entryDisabled(searchEntry: any): boolean { - const c = this.getAttr(searchEntry, 'userAccountControl'); - if (c != null) { - try { - const control = parseInt(c, null); - // tslint:disable-next-line - return (control & UserControlAccountDisabled) === UserControlAccountDisabled; - } catch (e) { - this.logService.error(e); - } - } - - return false; - } - - private async search(path: string, filter: string, processEntry: (searchEntry: any) => T, - controls: ldap.Control[] = []): Promise { - const options: ldap.SearchOptions = { - filter: filter, - scope: 'sub', - paged: this.dirConfig.pagedSearch, - }; - const entries: T[] = []; - return new Promise((resolve, reject) => { - this.client.search(path, options, controls, (err, res) => { - if (err != null) { - reject(err); - return; - } - - res.on('error', resErr => { - reject(resErr); - }); - - res.on('searchEntry', entry => { - const e = processEntry(entry); - if (e != null) { - entries.push(e); - } - }); - - res.on('end', result => { - resolve(entries); - }); + 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(); + } }); + } }); - } - - private async bind(): Promise { - return new Promise((resolve, reject) => { - if (this.dirConfig.hostname == null || this.dirConfig.port == null) { - reject(this.i18nService.t('dirConfigIncomplete')); - return; - } - const protocol = 'ldap' + (this.dirConfig.ssl && !this.dirConfig.startTls ? 's' : ''); - const url = protocol + '://' + this.dirConfig.hostname + - ':' + this.dirConfig.port; - const options: ldap.ClientOptions = { - url: url.trim().toLowerCase(), - }; - - const tlsOptions: any = {}; - if (this.dirConfig.ssl) { - if (this.dirConfig.sslAllowUnauthorized) { - tlsOptions.rejectUnauthorized = !this.dirConfig.sslAllowUnauthorized; - } - if (!this.dirConfig.startTls) { - if (this.dirConfig.sslCaPath != null && this.dirConfig.sslCaPath !== '' && - fs.existsSync(this.dirConfig.sslCaPath)) { - tlsOptions.ca = [fs.readFileSync(this.dirConfig.sslCaPath)]; - } - if (this.dirConfig.sslCertPath != null && this.dirConfig.sslCertPath !== '' && - fs.existsSync(this.dirConfig.sslCertPath)) { - tlsOptions.cert = fs.readFileSync(this.dirConfig.sslCertPath); - } - if (this.dirConfig.sslKeyPath != null && this.dirConfig.sslKeyPath !== '' && - fs.existsSync(this.dirConfig.sslKeyPath)) { - tlsOptions.key = fs.readFileSync(this.dirConfig.sslKeyPath); - } - } else { - if (this.dirConfig.tlsCaPath != null && this.dirConfig.tlsCaPath !== '' && - fs.existsSync(this.dirConfig.tlsCaPath)) { - tlsOptions.ca = [fs.readFileSync(this.dirConfig.tlsCaPath)]; - } - } - } - - tlsOptions.checkServerIdentity = this.checkServerIdentityAltNames; - options.tlsOptions = tlsOptions; - - this.client = ldap.createClient(options); - - const user = this.dirConfig.username == null || this.dirConfig.username.trim() === '' ? null : - this.dirConfig.username; - const pass = this.dirConfig.password == null || this.dirConfig.password.trim() === '' ? null : - this.dirConfig.password; - - if (user == null || pass == null) { - reject(this.i18nService.t('usernamePasswordNotConfigured')); - return; - } - - if (this.dirConfig.startTls && this.dirConfig.ssl) { - this.client.starttls(options.tlsOptions, undefined, (err, res) => { - if (err != null) { - reject(err.message); - } else { - this.client.bind(user, pass, err2 => { - if (err2 != null) { - reject(err2.message); - } else { - resolve(); - } - }); - } - }); - } else { - this.client.bind(user, pass, err => { - if (err != null) { - reject(err.message); - } else { - resolve(); - } - }); - } + } else { + this.client.bind(user, pass, (err) => { + if (err != null) { + reject(err.message); + } else { + resolve(); + } }); - } + } + }); + } - private async unbind(): Promise { - return new Promise((resolve, reject) => { - this.client.unbind(err => { - if (err != null) { - reject(err); - } else { - resolve(); - } - }); - }); - } - - private bufToGuid(buf: Buffer) { - const arr = new Uint8Array(buf); - const p1 = arr.slice(0, 4).reverse().buffer; - const p2 = arr.slice(4, 6).reverse().buffer; - const p3 = arr.slice(6, 8).reverse().buffer; - const p4 = arr.slice(8, 10).buffer; - const p5 = arr.slice(10).buffer; - const guid = Utils.fromBufferToHex(p1) + '-' + Utils.fromBufferToHex(p2) + '-' + Utils.fromBufferToHex(p3) + - '-' + Utils.fromBufferToHex(p4) + '-' + Utils.fromBufferToHex(p5); - return guid.toLowerCase(); - } - - private checkServerIdentityAltNames(host: string, cert: PeerCertificate) { - // Fixes the cert representation when subject is empty and altNames are present - // Required for node versions < 12.14.1 (which could be used for bwdc cli) - // Adapted from: https://github.com/auth0/ad-ldap-connector/commit/1f4dd2be6ed93dda591dd31ed5483a9b452a8d2a - // See https://github.com/nodejs/node/issues/11771 for details - if (cert && cert.subject == null && /(IP|DNS|URL)/.test(cert.subjectaltname)) { - cert.subject = { - C: null, - ST: null, - L: null, - O: null, - OU: null, - CN: null, - }; + private async unbind(): Promise { + return new Promise((resolve, reject) => { + this.client.unbind((err) => { + if (err != null) { + reject(err); + } else { + resolve(); } + }); + }); + } - return checkServerIdentity(host, cert); + private bufToGuid(buf: Buffer) { + const arr = new Uint8Array(buf); + const p1 = arr.slice(0, 4).reverse().buffer; + const p2 = arr.slice(4, 6).reverse().buffer; + const p3 = arr.slice(6, 8).reverse().buffer; + const p4 = arr.slice(8, 10).buffer; + const p5 = arr.slice(10).buffer; + const guid = + Utils.fromBufferToHex(p1) + + "-" + + Utils.fromBufferToHex(p2) + + "-" + + Utils.fromBufferToHex(p3) + + "-" + + Utils.fromBufferToHex(p4) + + "-" + + Utils.fromBufferToHex(p5); + return guid.toLowerCase(); + } + + private checkServerIdentityAltNames(host: string, cert: PeerCertificate) { + // Fixes the cert representation when subject is empty and altNames are present + // Required for node versions < 12.14.1 (which could be used for bwdc cli) + // Adapted from: https://github.com/auth0/ad-ldap-connector/commit/1f4dd2be6ed93dda591dd31ed5483a9b452a8d2a + // See https://github.com/nodejs/node/issues/11771 for details + if (cert && cert.subject == null && /(IP|DNS|URL)/.test(cert.subjectaltname)) { + cert.subject = { + C: null, + ST: null, + L: null, + O: null, + OU: null, + CN: null, + }; } + + return checkServerIdentity(host, cert); + } } diff --git a/src/services/lowdbStorage.service.ts b/src/services/lowdbStorage.service.ts index 692b7adb..54ff1dee 100644 --- a/src/services/lowdbStorage.service.ts +++ b/src/services/lowdbStorage.service.ts @@ -1,28 +1,34 @@ -import * as lock from 'proper-lockfile'; +import * as lock from "proper-lockfile"; -import { LogService } from 'jslib-common/abstractions/log.service'; +import { LogService } from "jslib-common/abstractions/log.service"; -import { LowdbStorageService as LowdbStorageServiceBase } from 'jslib-node/services/lowdbStorage.service'; +import { LowdbStorageService as LowdbStorageServiceBase } from "jslib-node/services/lowdbStorage.service"; -import { Utils } from 'jslib-common/misc/utils'; +import { Utils } from "jslib-common/misc/utils"; export class LowdbStorageService extends LowdbStorageServiceBase { - constructor(logService: LogService, defaults?: any, dir?: string, allowCache = false, private requireLock = false) { - super(logService, defaults, dir, allowCache); - } + constructor( + logService: LogService, + defaults?: any, + dir?: string, + allowCache = false, + private requireLock = false + ) { + super(logService, defaults, dir, allowCache); + } - protected async lockDbFile(action: () => T): Promise { - if (this.requireLock && !Utils.isNullOrWhitespace(this.dataFilePath)) { - this.logService.info('acquiring db file lock'); - return await lock.lock(this.dataFilePath, { retries: 3 }).then(release => { - try { - return action(); - } finally { - release(); - } - }); - } else { - return action(); + protected async lockDbFile(action: () => T): Promise { + if (this.requireLock && !Utils.isNullOrWhitespace(this.dataFilePath)) { + this.logService.info("acquiring db file lock"); + return await lock.lock(this.dataFilePath, { retries: 3 }).then((release) => { + try { + return action(); + } finally { + release(); } + }); + } else { + return action(); } + } } diff --git a/src/services/nodeApi.service.ts b/src/services/nodeApi.service.ts index 8507fde3..b41c5e3a 100644 --- a/src/services/nodeApi.service.ts +++ b/src/services/nodeApi.service.ts @@ -1,17 +1,30 @@ -import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; -import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; -import { TokenService } from 'jslib-common/abstractions/token.service'; +import { EnvironmentService } from "jslib-common/abstractions/environment.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; +import { TokenService } from "jslib-common/abstractions/token.service"; -import { NodeApiService as NodeApiServiceBase } from 'jslib-node/services/nodeApi.service'; +import { NodeApiService as NodeApiServiceBase } from "jslib-node/services/nodeApi.service"; export class NodeApiService extends NodeApiServiceBase { - constructor(tokenService: TokenService, platformUtilsService: PlatformUtilsService, environmentService: EnvironmentService, - private refreshTokenCallback: () => Promise, logoutCallback: (expired: boolean) => Promise, - customUserAgent: string = null, apiKeyRefresh: (clientId: string, clientSecret: string) => Promise) { - super(tokenService, platformUtilsService, environmentService, logoutCallback, customUserAgent, apiKeyRefresh); - } + constructor( + tokenService: TokenService, + platformUtilsService: PlatformUtilsService, + environmentService: EnvironmentService, + private refreshTokenCallback: () => Promise, + logoutCallback: (expired: boolean) => Promise, + customUserAgent: string = null, + apiKeyRefresh: (clientId: string, clientSecret: string) => Promise + ) { + super( + tokenService, + platformUtilsService, + environmentService, + logoutCallback, + customUserAgent, + apiKeyRefresh + ); + } - doRefreshToken(): Promise { - return this.refreshTokenCallback(); - } + doRefreshToken(): Promise { + return this.refreshTokenCallback(); + } } diff --git a/src/services/okta-directory.service.ts b/src/services/okta-directory.service.ts index 4f80d6f5..11fb22bb 100644 --- a/src/services/okta-directory.service.ts +++ b/src/services/okta-directory.service.ts @@ -1,253 +1,271 @@ -import { DirectoryType } from '../enums/directoryType'; +import { DirectoryType } from "../enums/directoryType"; -import { GroupEntry } from '../models/groupEntry'; -import { OktaConfiguration } from '../models/oktaConfiguration'; -import { SyncConfiguration } from '../models/syncConfiguration'; -import { UserEntry } from '../models/userEntry'; +import { GroupEntry } from "../models/groupEntry"; +import { OktaConfiguration } from "../models/oktaConfiguration"; +import { SyncConfiguration } from "../models/syncConfiguration"; +import { UserEntry } from "../models/userEntry"; -import { BaseDirectoryService } from './baseDirectory.service'; -import { ConfigurationService } from './configuration.service'; -import { IDirectoryService } from './directory.service'; +import { BaseDirectoryService } from "./baseDirectory.service"; +import { ConfigurationService } from "./configuration.service"; +import { IDirectoryService } from "./directory.service"; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { LogService } from 'jslib-common/abstractions/log.service'; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { LogService } from "jslib-common/abstractions/log.service"; -import * as https from 'https'; +import * as https from "https"; const DelayBetweenBuildGroupCallsInMilliseconds = 500; export class OktaDirectoryService extends BaseDirectoryService implements IDirectoryService { - private dirConfig: OktaConfiguration; - private syncConfig: SyncConfiguration; - private lastBuildGroupCall: number; + private dirConfig: OktaConfiguration; + private syncConfig: SyncConfiguration; + private lastBuildGroupCall: number; - constructor(private configurationService: ConfigurationService, private logService: LogService, - private i18nService: I18nService) { - super(); + 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.Okta) { + return; } - async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> { - const type = await this.configurationService.getDirectoryType(); - if (type !== DirectoryType.Okta) { - return; - } - - this.dirConfig = await this.configurationService.getDirectory(DirectoryType.Okta); - if (this.dirConfig == null) { - return; - } - - this.syncConfig = await this.configurationService.getSync(); - if (this.syncConfig == null) { - return; - } - - if (this.dirConfig.orgUrl == null || this.dirConfig.token == null) { - throw new Error(this.i18nService.t('dirConfigIncomplete')); - } - - let users: UserEntry[]; - if (this.syncConfig.users) { - users = await this.getUsers(force); - } - - let groups: GroupEntry[]; - if (this.syncConfig.groups) { - const setFilter = this.createCustomSet(this.syncConfig.groupFilter); - groups = await this.getGroups(this.forceGroup(force, users), setFilter); - users = this.filterUsersFromGroupsSet(users, groups, setFilter, this.syncConfig); - } - - return [groups, users]; + this.dirConfig = await this.configurationService.getDirectory( + DirectoryType.Okta + ); + if (this.dirConfig == null) { + return; } - private async getUsers(force: boolean): Promise { - const entries: UserEntry[] = []; - const lastSync = await this.configurationService.getLastUserSyncDate(); - const oktaFilter = this.buildOktaFilter(this.syncConfig.userFilter, force, lastSync); - const setFilter = this.createCustomSet(this.syncConfig.userFilter); + this.syncConfig = await this.configurationService.getSync(); + if (this.syncConfig == null) { + return; + } - this.logService.info('Querying users.'); - const usersPromise = this.apiGetMany('users?filter=' + this.encodeUrlParameter(oktaFilter)) - .then((users: any[]) => { - for (const user of users) { - const entry = this.buildUser(user); - if (entry != null && !this.filterOutResult(setFilter, entry.email)) { - entries.push(entry); - } + if (this.dirConfig.orgUrl == null || this.dirConfig.token == null) { + throw new Error(this.i18nService.t("dirConfigIncomplete")); + } + + let users: UserEntry[]; + if (this.syncConfig.users) { + users = await this.getUsers(force); + } + + let groups: GroupEntry[]; + if (this.syncConfig.groups) { + const setFilter = this.createCustomSet(this.syncConfig.groupFilter); + groups = await this.getGroups(this.forceGroup(force, users), setFilter); + users = this.filterUsersFromGroupsSet(users, groups, setFilter, this.syncConfig); + } + + return [groups, users]; + } + + private async getUsers(force: boolean): Promise { + const entries: UserEntry[] = []; + const lastSync = await this.configurationService.getLastUserSyncDate(); + const oktaFilter = this.buildOktaFilter(this.syncConfig.userFilter, force, lastSync); + const setFilter = this.createCustomSet(this.syncConfig.userFilter); + + this.logService.info("Querying users."); + const usersPromise = this.apiGetMany( + "users?filter=" + this.encodeUrlParameter(oktaFilter) + ).then((users: any[]) => { + for (const user of users) { + const entry = this.buildUser(user); + if (entry != null && !this.filterOutResult(setFilter, entry.email)) { + entries.push(entry); + } + } + }); + + // Deactivated users have to be queried for separately, only when no filter is provided in the first query + let deactUsersPromise: any; + if (oktaFilter == null || oktaFilter.indexOf("lastUpdated ") === -1) { + let deactOktaFilter = 'status eq "DEPROVISIONED"'; + if (oktaFilter != null) { + deactOktaFilter = "(" + oktaFilter + ") and " + deactOktaFilter; + } + deactUsersPromise = this.apiGetMany( + "users?filter=" + this.encodeUrlParameter(deactOktaFilter) + ).then((users: any[]) => { + for (const user of users) { + const entry = this.buildUser(user); + if (entry != null && !this.filterOutResult(setFilter, entry.email)) { + entries.push(entry); + } + } + }); + } else { + deactUsersPromise = Promise.resolve(); + } + + await Promise.all([usersPromise, deactUsersPromise]); + return entries; + } + + private buildUser(user: any) { + const entry = new UserEntry(); + entry.externalId = user.id; + entry.referenceId = user.id; + entry.email = user.profile.email != null ? user.profile.email.trim().toLowerCase() : null; + entry.deleted = user.status === "DEPROVISIONED"; + entry.disabled = user.status === "SUSPENDED"; + return entry; + } + + private async getGroups( + force: boolean, + setFilter: [boolean, Set] + ): Promise { + const entries: GroupEntry[] = []; + const lastSync = await this.configurationService.getLastGroupSyncDate(); + const oktaFilter = this.buildOktaFilter(this.syncConfig.groupFilter, force, lastSync); + + this.logService.info("Querying groups."); + await this.apiGetMany("groups?filter=" + this.encodeUrlParameter(oktaFilter)).then( + async (groups: any[]) => { + for (const group of groups.filter( + (g) => !this.filterOutResult(setFilter, g.profile.name) + )) { + const entry = await this.buildGroup(group); + if (entry != null) { + entries.push(entry); + } + } + } + ); + return entries; + } + + private async buildGroup(group: any): Promise { + const entry = new GroupEntry(); + entry.externalId = group.id; + entry.referenceId = group.id; + entry.name = group.profile.name; + + // throttle some to avoid rate limiting + const neededDelay = + DelayBetweenBuildGroupCallsInMilliseconds - (Date.now() - this.lastBuildGroupCall); + if (neededDelay > 0) { + await new Promise((resolve) => setTimeout(resolve, neededDelay)); + } + this.lastBuildGroupCall = Date.now(); + + await this.apiGetMany("groups/" + group.id + "/users").then((users: any[]) => { + for (const user of users) { + entry.userMemberExternalIds.add(user.id); + } + }); + + return entry; + } + + private buildOktaFilter(baseFilter: string, force: boolean, lastSync: Date) { + baseFilter = this.createDirectoryQuery(baseFilter); + baseFilter = baseFilter == null || baseFilter.trim() === "" ? null : baseFilter; + if (force || lastSync == null) { + return baseFilter; + } + + const updatedFilter = 'lastUpdated gt "' + lastSync.toISOString() + '"'; + if (baseFilter == null) { + return updatedFilter; + } + + return "(" + baseFilter + ") and " + updatedFilter; + } + + private encodeUrlParameter(filter: string): string { + return filter == null ? "" : encodeURIComponent(filter); + } + + private async apiGetCall(url: string): Promise<[any, Map]> { + const u = new URL(url); + return new Promise((resolve) => { + https.get( + { + hostname: u.hostname, + path: u.pathname + u.search, + port: 443, + headers: { + Authorization: "SSWS " + this.dirConfig.token, + Accept: "application/json", + }, + }, + (res) => { + let body = ""; + + res.on("data", (chunk) => { + body += chunk; + }); + + res.on("end", () => { + if (res.statusCode !== 200) { + resolve(null); + return; + } + + const responseJson = JSON.parse(body); + if (res.headers != null) { + const headersMap = new Map(); + for (const key in res.headers) { + if (res.headers.hasOwnProperty(key)) { + const val = res.headers[key]; + headersMap.set(key.toLowerCase(), val); } - }); - - // Deactivated users have to be queried for separately, only when no filter is provided in the first query - let deactUsersPromise: any; - if (oktaFilter == null || oktaFilter.indexOf('lastUpdated ') === -1) { - let deactOktaFilter = 'status eq "DEPROVISIONED"'; - if (oktaFilter != null) { - deactOktaFilter = '(' + oktaFilter + ') and ' + deactOktaFilter; + } + resolve([responseJson, headersMap]); + return; } - deactUsersPromise = this.apiGetMany('users?filter=' + this.encodeUrlParameter(deactOktaFilter)) - .then((users: any[]) => { - for (const user of users) { - const entry = this.buildUser(user); - if (entry != null && !this.filterOutResult(setFilter, entry.email)) { - entries.push(entry); - } - } - }); - } else { - deactUsersPromise = Promise.resolve(); - } + resolve([responseJson, null]); + }); - await Promise.all([usersPromise, deactUsersPromise]); - return entries; + res.on("error", () => { + resolve(null); + }); + } + ); + }); + } + + private async apiGetMany(endpoint: string, currentData: any[] = []): Promise { + const url = + endpoint.indexOf("https://") === 0 ? endpoint : `${this.dirConfig.orgUrl}/api/v1/${endpoint}`; + const response = await this.apiGetCall(url); + if (response == null || response[0] == null || !Array.isArray(response[0])) { + throw new Error("API call failed."); } - - private buildUser(user: any) { - const entry = new UserEntry(); - entry.externalId = user.id; - entry.referenceId = user.id; - entry.email = user.profile.email != null ? user.profile.email.trim().toLowerCase() : null; - entry.deleted = user.status === 'DEPROVISIONED'; - entry.disabled = user.status === 'SUSPENDED'; - return entry; + if (response[0].length === 0) { + return currentData; } - - private async getGroups(force: boolean, setFilter: [boolean, Set]): Promise { - const entries: GroupEntry[] = []; - const lastSync = await this.configurationService.getLastGroupSyncDate(); - const oktaFilter = this.buildOktaFilter(this.syncConfig.groupFilter, force, lastSync); - - this.logService.info('Querying groups.'); - await this.apiGetMany('groups?filter=' + this.encodeUrlParameter(oktaFilter)).then(async (groups: any[]) => { - for (const group of groups.filter(g => !this.filterOutResult(setFilter, g.profile.name))) { - const entry = await this.buildGroup(group); - if (entry != null) { - entries.push(entry); - } - } - }); - return entries; + currentData = currentData.concat(response[0]); + if (response[1] == null) { + return currentData; } - - private async buildGroup(group: any): Promise { - const entry = new GroupEntry(); - entry.externalId = group.id; - entry.referenceId = group.id; - entry.name = group.profile.name; - - // throttle some to avoid rate limiting - const neededDelay = DelayBetweenBuildGroupCallsInMilliseconds - (Date.now() - this.lastBuildGroupCall); - if (neededDelay > 0) { - await new Promise(resolve => - setTimeout(resolve, neededDelay)); - } - this.lastBuildGroupCall = Date.now(); - - await this.apiGetMany('groups/' + group.id + '/users').then((users: any[]) => { - for (const user of users) { - entry.userMemberExternalIds.add(user.id); - } - }); - - return entry; + const linkHeader = response[1].get("link"); + if (linkHeader == null || Array.isArray(linkHeader)) { + return currentData; } - - private buildOktaFilter(baseFilter: string, force: boolean, lastSync: Date) { - baseFilter = this.createDirectoryQuery(baseFilter); - baseFilter = baseFilter == null || baseFilter.trim() === '' ? null : baseFilter; - if (force || lastSync == null) { - return baseFilter; + let nextLink: string = null; + const linkHeaderParts = linkHeader.split(","); + for (const part of linkHeaderParts) { + if (part.indexOf('; rel="next"') > -1) { + const subParts = part.split(";"); + if (subParts.length > 0 && subParts[0].indexOf("https://") > -1) { + nextLink = subParts[0].replace(">", "").replace("<", "").trim(); + break; } - - const updatedFilter = 'lastUpdated gt "' + lastSync.toISOString() + '"'; - if (baseFilter == null) { - return updatedFilter; - } - - return '(' + baseFilter + ') and ' + updatedFilter; + } } - - private encodeUrlParameter(filter: string): string { - return filter == null ? '' : encodeURIComponent(filter); - } - - private async apiGetCall(url: string): Promise<[any, Map]> { - const u = new URL(url); - return new Promise(resolve => { - https.get({ - hostname: u.hostname, - path: u.pathname + u.search, - port: 443, - headers: { - Authorization: 'SSWS ' + this.dirConfig.token, - Accept: 'application/json', - }, - }, res => { - let body = ''; - - res.on('data', chunk => { - body += chunk; - }); - - res.on('end', () => { - if (res.statusCode !== 200) { - resolve(null); - return; - } - - const responseJson = JSON.parse(body); - if (res.headers != null) { - const headersMap = new Map(); - for (const key in res.headers) { - if (res.headers.hasOwnProperty(key)) { - const val = res.headers[key]; - headersMap.set(key.toLowerCase(), val); - } - } - resolve([responseJson, headersMap]); - return; - } - resolve([responseJson, null]); - }); - - res.on('error', () => { - resolve(null); - }); - }); - }); - } - - private async apiGetMany(endpoint: string, currentData: any[] = []): Promise { - const url = endpoint.indexOf('https://') === 0 ? endpoint : `${this.dirConfig.orgUrl}/api/v1/${endpoint}`; - const response = await this.apiGetCall(url); - if (response == null || response[0] == null || !Array.isArray(response[0])) { - throw new Error('API call failed.'); - } - if (response[0].length === 0) { - return currentData; - } - currentData = currentData.concat(response[0]); - if (response[1] == null) { - return currentData; - } - const linkHeader = response[1].get('link'); - if (linkHeader == null || Array.isArray(linkHeader)) { - return currentData; - } - let nextLink: string = null; - const linkHeaderParts = linkHeader.split(','); - for (const part of linkHeaderParts) { - if (part.indexOf('; rel="next"') > -1) { - const subParts = part.split(';'); - if (subParts.length > 0 && subParts[0].indexOf('https://') > -1) { - nextLink = subParts[0].replace('>', '').replace('<', '').trim(); - break; - } - } - } - if (nextLink == null) { - return currentData; - } - return this.apiGetMany(nextLink, currentData); + if (nextLink == null) { + return currentData; } + return this.apiGetMany(nextLink, currentData); + } } diff --git a/src/services/onelogin-directory.service.ts b/src/services/onelogin-directory.service.ts index dd7995a4..4838fc35 100644 --- a/src/services/onelogin-directory.service.ts +++ b/src/services/onelogin-directory.service.ts @@ -1,195 +1,209 @@ -import { DirectoryType } from '../enums/directoryType'; +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 { GroupEntry } from "../models/groupEntry"; +import { OneLoginConfiguration } from "../models/oneLoginConfiguration"; +import { SyncConfiguration } from "../models/syncConfiguration"; +import { UserEntry } from "../models/userEntry"; -import { BaseDirectoryService } from './baseDirectory.service'; -import { ConfigurationService } from './configuration.service'; -import { IDirectoryService } from './directory.service'; +import { BaseDirectoryService } from "./baseDirectory.service"; +import { ConfigurationService } from "./configuration.service"; +import { IDirectoryService } from "./directory.service"; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { LogService } from 'jslib-common/abstractions/log.service'; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { LogService } from "jslib-common/abstractions/log.service"; // Basic email validation: something@something.something const ValidEmailRegex = /^\S+@\S+\.\S+$/; export class OneLoginDirectoryService extends BaseDirectoryService implements IDirectoryService { - private dirConfig: OneLoginConfiguration; - private syncConfig: SyncConfiguration; - private accessToken: string; - private allUsers: any[] = []; + private dirConfig: OneLoginConfiguration; + private syncConfig: SyncConfiguration; + private accessToken: string; + private allUsers: any[] = []; - constructor(private configurationService: ConfigurationService, private logService: LogService, - private i18nService: I18nService) { - super(); + 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; } - 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(DirectoryType.OneLogin); - if (this.dirConfig == null) { - return; - } - - this.syncConfig = await this.configurationService.getSync(); - if (this.syncConfig == null) { - return; - } - - if (this.dirConfig.clientId == null || this.dirConfig.clientSecret == null) { - throw new Error(this.i18nService.t('dirConfigIncomplete')); - } - - this.accessToken = await this.getAccessToken(); - if (this.accessToken == null) { - throw new Error('Could not get access token'); - } - - let users: UserEntry[]; - if (this.syncConfig.users) { - users = await this.getUsers(force); - } - - let groups: GroupEntry[]; - if (this.syncConfig.groups) { - const setFilter = this.createCustomSet(this.syncConfig.groupFilter); - groups = await this.getGroups(this.forceGroup(force, users), setFilter); - users = this.filterUsersFromGroupsSet(users, groups, setFilter, this.syncConfig); - } - - return [groups, users]; + this.dirConfig = await this.configurationService.getDirectory( + DirectoryType.OneLogin + ); + if (this.dirConfig == null) { + return; } - private async getUsers(force: boolean): Promise { - 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); + this.syncConfig = await this.configurationService.getSync(); + if (this.syncConfig == null) { + return; } - private buildUser(user: any) { - const entry = new UserEntry(); - entry.externalId = user.id; - entry.referenceId = user.id; - entry.deleted = false; - entry.disabled = user.status === 2; - entry.email = user.email; - if (!this.validEmailAddress(entry.email) && user.username != null && user.username !== '') { - if (this.validEmailAddress(user.username)) { - entry.email = user.username; - } else if (this.syncConfig.useEmailPrefixSuffix && this.syncConfig.emailSuffix != null) { - entry.email = user.username + this.syncConfig.emailSuffix; - } - } - if (entry.email != null) { - entry.email = entry.email.trim().toLowerCase(); - } - if (!this.validEmailAddress(entry.email)) { - return null; - } - return entry; + if (this.dirConfig.clientId == null || this.dirConfig.clientSecret == null) { + throw new Error(this.i18nService.t("dirConfigIncomplete")); } - private async getGroups(force: boolean, setFilter: [boolean, Set]): Promise { - 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); + this.accessToken = await this.getAccessToken(); + if (this.accessToken == null) { + throw new Error("Could not get access token"); } - 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; + let users: UserEntry[]; + if (this.syncConfig.users) { + users = await this.getUsers(force); } - 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; + let groups: GroupEntry[]; + if (this.syncConfig.groups) { + const setFilter = this.createCustomSet(this.syncConfig.groupFilter); + groups = await this.getGroups(this.forceGroup(force, users), setFilter); + users = this.filterUsersFromGroupsSet(users, groups, setFilter, this.syncConfig); } - private async apiGetCall(url: string): Promise { - 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 [groups, users]; + } + + private async getUsers(force: boolean): Promise { + const entries: UserEntry[] = []; + const query = this.createDirectoryQuery(this.syncConfig.userFilter); + const setFilter = this.createCustomSet(this.syncConfig.userFilter); + this.logService.info("Querying users."); + this.allUsers = await this.apiGetMany("users" + (query != null ? "?" + query : "")); + this.allUsers.forEach((user) => { + const entry = this.buildUser(user); + if (entry != null && !this.filterOutResult(setFilter, entry.email)) { + entries.push(entry); + } + }); + return Promise.resolve(entries); + } + + private buildUser(user: any) { + const entry = new UserEntry(); + entry.externalId = user.id; + entry.referenceId = user.id; + entry.deleted = false; + entry.disabled = user.status === 2; + entry.email = user.email; + if (!this.validEmailAddress(entry.email) && user.username != null && user.username !== "") { + if (this.validEmailAddress(user.username)) { + entry.email = user.username; + } else if (this.syncConfig.useEmailPrefixSuffix && this.syncConfig.emailSuffix != null) { + entry.email = user.username + this.syncConfig.emailSuffix; + } + } + if (entry.email != null) { + entry.email = entry.email.trim().toLowerCase(); + } + if (!this.validEmailAddress(entry.email)) { + return null; + } + return entry; + } + + private async getGroups( + force: boolean, + setFilter: [boolean, Set] + ): Promise { + 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 null; + }); } - private async apiGetMany(endpoint: string, currentData: any[] = []): Promise { - 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); - } + return entry; + } - private validEmailAddress(email: string) { - return email != null && email !== '' && ValidEmailRegex.test(email); + 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 { + 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 { + const url = + endpoint.indexOf("https://") === 0 + ? endpoint + : `https://api.${this.dirConfig.region}.onelogin.com/api/1/${endpoint}`; + const response = await this.apiGetCall(url); + if (response == null || response.status == null || response.data == null) { + return currentData; + } + if (response.status.code !== 200) { + throw new Error("API call failed."); + } + currentData = currentData.concat(response.data); + if (response.pagination == null || response.pagination.next_link == null) { + return currentData; + } + return this.apiGetMany(response.pagination.next_link, currentData); + } + + private validEmailAddress(email: string) { + return email != null && email !== "" && ValidEmailRegex.test(email); + } } diff --git a/src/services/sync.service.ts b/src/services/sync.service.ts index 81623c85..0fede353 100644 --- a/src/services/sync.service.ts +++ b/src/services/sync.service.ts @@ -1,220 +1,272 @@ -import { DirectoryType } from '../enums/directoryType'; +import { DirectoryType } from "../enums/directoryType"; -import { GroupEntry } from '../models/groupEntry'; -import { SyncConfiguration } from '../models/syncConfiguration'; -import { UserEntry } from '../models/userEntry'; +import { GroupEntry } from "../models/groupEntry"; +import { SyncConfiguration } from "../models/syncConfiguration"; +import { UserEntry } from "../models/userEntry"; -import { OrganizationImportRequest } from 'jslib-common/models/request/organizationImportRequest'; +import { OrganizationImportRequest } from "jslib-common/models/request/organizationImportRequest"; -import { ApiService } from 'jslib-common/abstractions/api.service'; -import { CryptoFunctionService } from 'jslib-common/abstractions/cryptoFunction.service'; -import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { LogService } from 'jslib-common/abstractions/log.service'; -import { MessagingService } from 'jslib-common/abstractions/messaging.service'; +import { ApiService } from "jslib-common/abstractions/api.service"; +import { CryptoFunctionService } from "jslib-common/abstractions/cryptoFunction.service"; +import { EnvironmentService } from "jslib-common/abstractions/environment.service"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { LogService } from "jslib-common/abstractions/log.service"; +import { MessagingService } from "jslib-common/abstractions/messaging.service"; -import { Utils } from 'jslib-common/misc/utils'; +import { Utils } from "jslib-common/misc/utils"; -import { AzureDirectoryService } from './azure-directory.service'; -import { ConfigurationService } from './configuration.service'; -import { IDirectoryService } from './directory.service'; -import { GSuiteDirectoryService } from './gsuite-directory.service'; -import { LdapDirectoryService } from './ldap-directory.service'; -import { OktaDirectoryService } from './okta-directory.service'; -import { OneLoginDirectoryService } from './onelogin-directory.service'; +import { AzureDirectoryService } from "./azure-directory.service"; +import { ConfigurationService } from "./configuration.service"; +import { IDirectoryService } from "./directory.service"; +import { GSuiteDirectoryService } from "./gsuite-directory.service"; +import { LdapDirectoryService } from "./ldap-directory.service"; +import { OktaDirectoryService } from "./okta-directory.service"; +import { OneLoginDirectoryService } from "./onelogin-directory.service"; export class SyncService { - private dirType: DirectoryType; + private dirType: DirectoryType; - constructor(private configurationService: ConfigurationService, private logService: LogService, - private cryptoFunctionService: CryptoFunctionService, private apiService: ApiService, - private messagingService: MessagingService, private i18nService: I18nService, - private environmentService: EnvironmentService) { } + constructor( + private configurationService: ConfigurationService, + private logService: LogService, + private cryptoFunctionService: CryptoFunctionService, + private apiService: ApiService, + private messagingService: MessagingService, + private i18nService: I18nService, + private environmentService: EnvironmentService + ) {} - async sync(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> { - this.dirType = await this.configurationService.getDirectoryType(); - if (this.dirType == null) { - throw new Error('No directory configured.'); - } - - const directoryService = this.getDirectoryService(); - if (directoryService == null) { - throw new Error('Cannot load directory service.'); - } - - const syncConfig = await this.configurationService.getSync(); - const startingGroupDelta = await this.configurationService.getGroupDeltaToken(); - const startingUserDelta = await this.configurationService.getUserDeltaToken(); - const now = new Date(); - - this.messagingService.send('dirSyncStarted'); - try { - const entries = await directoryService.getEntries(force || syncConfig.overwriteExisting, test); - let groups = entries[0]; - let users = this.filterUnsupportedUsers(entries[1]); - - if (groups != null && groups.length > 0) { - this.flattenUsersToGroups(groups, groups); - } - - users = this.removeDuplicateUsers(users); - - if (test || (!syncConfig.overwriteExisting && - (groups == null || groups.length === 0) && (users == null || users.length === 0))) { - if (!test) { - await this.saveSyncTimes(syncConfig, now); - } - - this.messagingService.send('dirSyncCompleted', { successfully: true }); - return [groups, users]; - } - - const req = this.buildRequest(groups, users, syncConfig.removeDisabled, syncConfig.overwriteExisting, syncConfig.largeImport); - const reqJson = JSON.stringify(req); - - const orgId = await this.configurationService.getOrganizationId(); - if (orgId == null) { - throw new Error('Organization not set.'); - } - - // TODO: Remove hashLegacy once we're sure clients have had time to sync new hashes - let hashLegacy: string = null; - const hashBuffLegacy = await this.cryptoFunctionService.hash(this.environmentService.getApiUrl() + reqJson, 'sha256'); - if (hashBuffLegacy != null) { - hashLegacy = Utils.fromBufferToB64(hashBuffLegacy); - } - let hash: string = null; - const hashBuff = await this.cryptoFunctionService.hash(this.environmentService.getApiUrl() + orgId + reqJson, 'sha256'); - if (hashBuff != null) { - hash = Utils.fromBufferToB64(hashBuff); - } - const lastHash = await this.configurationService.getLastSyncHash(); - - if (lastHash == null || (hash !== lastHash && hashLegacy !== lastHash)) { - await this.apiService.postPublicImportDirectory(req); - await this.configurationService.saveLastSyncHash(hash); - } else { - groups = null; - users = null; - } - - await this.saveSyncTimes(syncConfig, now); - this.messagingService.send('dirSyncCompleted', { successfully: true }); - return [groups, users]; - } catch (e) { - if (!test) { - await this.configurationService.saveGroupDeltaToken(startingGroupDelta); - await this.configurationService.saveUserDeltaToken(startingUserDelta); - } - - this.messagingService.send('dirSyncCompleted', { successfully: false }); - throw e; - } + async sync(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> { + this.dirType = await this.configurationService.getDirectoryType(); + if (this.dirType == null) { + throw new Error("No directory configured."); } - private removeDuplicateUsers(users: UserEntry[]) { - const uniqueUsers = new Array(); - const processedActiveUsers = new Map(); - const processedDeletedUsers = new Map(); - const duplicateEmails = new Array(); - - // UserEntrys with the same email are ignored if their properties are the same - // UserEntrys with the same email but different properties will throw an error, unless they are all in a deleted state. - users.forEach(u => { - if (processedActiveUsers.has(u.email)) { - if (processedActiveUsers.get(u.email) !== JSON.stringify(u)) { - duplicateEmails.push(u.email); - } - } else { - if (!u.deleted) { - // Check that active UserEntry does not conflict with a deleted UserEntry - if (processedDeletedUsers.has(u.email)) { - duplicateEmails.push(u.email); - } else { - processedActiveUsers.set(u.email, JSON.stringify(u)); - uniqueUsers.push(u); - } - } else { - // UserEntrys with duplicate email will not throw an error if they are all deleted. They will be synced. - processedDeletedUsers.set(u.email, JSON.stringify(u)); - uniqueUsers.push(u); - } - } - }); - - if (duplicateEmails.length > 0) { - const emailsMessage = duplicateEmails.length < 4 ? - duplicateEmails.join('\n') : - duplicateEmails.slice(0, 3).join('\n') + '\n' + this.i18nService.t('andMore', `${duplicateEmails.length - 3}`); - throw new Error(this.i18nService.t('duplicateEmails') + '\n' + emailsMessage); - } - - return uniqueUsers; + const directoryService = this.getDirectoryService(); + if (directoryService == null) { + throw new Error("Cannot load directory service."); } - private filterUnsupportedUsers(users: UserEntry[]): UserEntry[] { - return users == null ? null : users.filter(u => u.email?.length <= 256); + const syncConfig = await this.configurationService.getSync(); + const startingGroupDelta = await this.configurationService.getGroupDeltaToken(); + const startingUserDelta = await this.configurationService.getUserDeltaToken(); + const now = new Date(); + + this.messagingService.send("dirSyncStarted"); + try { + const entries = await directoryService.getEntries( + force || syncConfig.overwriteExisting, + test + ); + let groups = entries[0]; + let users = this.filterUnsupportedUsers(entries[1]); + + if (groups != null && groups.length > 0) { + this.flattenUsersToGroups(groups, groups); + } + + users = this.removeDuplicateUsers(users); + + if ( + test || + (!syncConfig.overwriteExisting && + (groups == null || groups.length === 0) && + (users == null || users.length === 0)) + ) { + if (!test) { + await this.saveSyncTimes(syncConfig, now); + } + + this.messagingService.send("dirSyncCompleted", { successfully: true }); + return [groups, users]; + } + + const req = this.buildRequest( + groups, + users, + syncConfig.removeDisabled, + syncConfig.overwriteExisting, + syncConfig.largeImport + ); + const reqJson = JSON.stringify(req); + + const orgId = await this.configurationService.getOrganizationId(); + if (orgId == null) { + throw new Error("Organization not set."); + } + + // TODO: Remove hashLegacy once we're sure clients have had time to sync new hashes + let hashLegacy: string = null; + const hashBuffLegacy = await this.cryptoFunctionService.hash( + this.environmentService.getApiUrl() + reqJson, + "sha256" + ); + if (hashBuffLegacy != null) { + hashLegacy = Utils.fromBufferToB64(hashBuffLegacy); + } + let hash: string = null; + const hashBuff = await this.cryptoFunctionService.hash( + this.environmentService.getApiUrl() + orgId + reqJson, + "sha256" + ); + if (hashBuff != null) { + hash = Utils.fromBufferToB64(hashBuff); + } + const lastHash = await this.configurationService.getLastSyncHash(); + + if (lastHash == null || (hash !== lastHash && hashLegacy !== lastHash)) { + await this.apiService.postPublicImportDirectory(req); + await this.configurationService.saveLastSyncHash(hash); + } else { + groups = null; + users = null; + } + + await this.saveSyncTimes(syncConfig, now); + this.messagingService.send("dirSyncCompleted", { successfully: true }); + return [groups, users]; + } catch (e) { + if (!test) { + await this.configurationService.saveGroupDeltaToken(startingGroupDelta); + await this.configurationService.saveUserDeltaToken(startingUserDelta); + } + + this.messagingService.send("dirSyncCompleted", { successfully: false }); + throw e; + } + } + + private removeDuplicateUsers(users: UserEntry[]) { + const uniqueUsers = new Array(); + const processedActiveUsers = new Map(); + const processedDeletedUsers = new Map(); + const duplicateEmails = new Array(); + + // UserEntrys with the same email are ignored if their properties are the same + // UserEntrys with the same email but different properties will throw an error, unless they are all in a deleted state. + users.forEach((u) => { + if (processedActiveUsers.has(u.email)) { + if (processedActiveUsers.get(u.email) !== JSON.stringify(u)) { + duplicateEmails.push(u.email); + } + } else { + if (!u.deleted) { + // Check that active UserEntry does not conflict with a deleted UserEntry + if (processedDeletedUsers.has(u.email)) { + duplicateEmails.push(u.email); + } else { + processedActiveUsers.set(u.email, JSON.stringify(u)); + uniqueUsers.push(u); + } + } else { + // UserEntrys with duplicate email will not throw an error if they are all deleted. They will be synced. + processedDeletedUsers.set(u.email, JSON.stringify(u)); + uniqueUsers.push(u); + } + } + }); + + if (duplicateEmails.length > 0) { + const emailsMessage = + duplicateEmails.length < 4 + ? duplicateEmails.join("\n") + : duplicateEmails.slice(0, 3).join("\n") + + "\n" + + this.i18nService.t("andMore", `${duplicateEmails.length - 3}`); + throw new Error(this.i18nService.t("duplicateEmails") + "\n" + emailsMessage); } - private flattenUsersToGroups(levelGroups: GroupEntry[], allGroups: GroupEntry[]): Set { - let allUsers = new Set(); - if (allGroups == null) { - return allUsers; - } - for (const group of levelGroups) { - const childGroups = allGroups.filter(g => group.groupMemberReferenceIds.has(g.referenceId)); - const childUsers = this.flattenUsersToGroups(childGroups, allGroups); - childUsers.forEach(id => group.userMemberExternalIds.add(id)); - allUsers = new Set([...allUsers, ...group.userMemberExternalIds]); - } - return allUsers; - } + return uniqueUsers; + } - private getDirectoryService(): IDirectoryService { - switch (this.dirType) { - case DirectoryType.GSuite: - return new GSuiteDirectoryService(this.configurationService, this.logService, this.i18nService); - case DirectoryType.AzureActiveDirectory: - return new AzureDirectoryService(this.configurationService, this.logService, this.i18nService); - case DirectoryType.Ldap: - return new LdapDirectoryService(this.configurationService, this.logService, this.i18nService); - case DirectoryType.Okta: - return new OktaDirectoryService(this.configurationService, this.logService, this.i18nService); - case DirectoryType.OneLogin: - return new OneLoginDirectoryService(this.configurationService, this.logService, this.i18nService); - default: - return null; - } - } + private filterUnsupportedUsers(users: UserEntry[]): UserEntry[] { + return users == null ? null : users.filter((u) => u.email?.length <= 256); + } - private buildRequest(groups: GroupEntry[], users: UserEntry[], removeDisabled: boolean, overwriteExisting: boolean, - largeImport: boolean = false) { - return new OrganizationImportRequest({ - groups: (groups ?? []).map(g => { - return { - name: g.name, - externalId: g.externalId, - memberExternalIds: Array.from(g.userMemberExternalIds), - }; - }), - users: (users ?? []).map(u => { - return { - email: u.email, - externalId: u.externalId, - deleted: u.deleted || (removeDisabled && u.disabled), - }; - }), - overwriteExisting: overwriteExisting, - largeImport: largeImport, - }); + private flattenUsersToGroups(levelGroups: GroupEntry[], allGroups: GroupEntry[]): Set { + let allUsers = new Set(); + if (allGroups == null) { + return allUsers; } + for (const group of levelGroups) { + const childGroups = allGroups.filter((g) => group.groupMemberReferenceIds.has(g.referenceId)); + const childUsers = this.flattenUsersToGroups(childGroups, allGroups); + childUsers.forEach((id) => group.userMemberExternalIds.add(id)); + allUsers = new Set([...allUsers, ...group.userMemberExternalIds]); + } + return allUsers; + } - private async saveSyncTimes(syncConfig: SyncConfiguration, time: Date) { - if (syncConfig.groups) { - await this.configurationService.saveLastGroupSyncDate(time); - } - if (syncConfig.users) { - await this.configurationService.saveLastUserSyncDate(time); - } + private getDirectoryService(): IDirectoryService { + switch (this.dirType) { + case DirectoryType.GSuite: + return new GSuiteDirectoryService( + this.configurationService, + this.logService, + this.i18nService + ); + case DirectoryType.AzureActiveDirectory: + return new AzureDirectoryService( + this.configurationService, + this.logService, + this.i18nService + ); + case DirectoryType.Ldap: + return new LdapDirectoryService( + this.configurationService, + this.logService, + this.i18nService + ); + case DirectoryType.Okta: + return new OktaDirectoryService( + this.configurationService, + this.logService, + this.i18nService + ); + case DirectoryType.OneLogin: + return new OneLoginDirectoryService( + this.configurationService, + this.logService, + this.i18nService + ); + default: + return null; } + } + + private buildRequest( + groups: GroupEntry[], + users: UserEntry[], + removeDisabled: boolean, + overwriteExisting: boolean, + largeImport: boolean = false + ) { + return new OrganizationImportRequest({ + groups: (groups ?? []).map((g) => { + return { + name: g.name, + externalId: g.externalId, + memberExternalIds: Array.from(g.userMemberExternalIds), + }; + }), + users: (users ?? []).map((u) => { + return { + email: u.email, + externalId: u.externalId, + deleted: u.deleted || (removeDisabled && u.disabled), + }; + }), + overwriteExisting: overwriteExisting, + largeImport: largeImport, + }); + } + + private async saveSyncTimes(syncConfig: SyncConfiguration, time: Date) { + if (syncConfig.groups) { + await this.configurationService.saveLastGroupSyncDate(time); + } + if (syncConfig.users) { + await this.configurationService.saveLastUserSyncDate(time); + } + } } diff --git a/src/utils.ts b/src/utils.ts index d1babcf9..2fa34698 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,100 +1,105 @@ -import { I18nService } from 'jslib-common/abstractions/i18n.service'; +import { I18nService } from "jslib-common/abstractions/i18n.service"; -import { SyncService } from './services/sync.service'; +import { SyncService } from "./services/sync.service"; -import { Entry } from './models/entry'; -import { LdapConfiguration } from './models/ldapConfiguration'; -import { SimResult } from './models/simResult'; -import { SyncConfiguration } from './models/syncConfiguration'; -import { UserEntry } from './models/userEntry'; +import { Entry } from "./models/entry"; +import { LdapConfiguration } from "./models/ldapConfiguration"; +import { SimResult } from "./models/simResult"; +import { SyncConfiguration } from "./models/syncConfiguration"; +import { UserEntry } from "./models/userEntry"; export class ConnectorUtils { - static async simulate(syncService: SyncService, i18nService: I18nService, sinceLast: boolean): Promise { - return new Promise(async (resolve, reject) => { - const simResult = new SimResult(); - try { - const result = await syncService.sync(!sinceLast, true); - if (result[0] != null) { - simResult.groups = result[0]; - } - if (result[1] != null) { - simResult.users = result[1]; - } - } catch (e) { - simResult.groups = null; - simResult.users = null; - reject(e || i18nService.t('syncError')); - return; - } + static async simulate( + syncService: SyncService, + i18nService: I18nService, + sinceLast: boolean + ): Promise { + return new Promise(async (resolve, reject) => { + const simResult = new SimResult(); + try { + const result = await syncService.sync(!sinceLast, true); + if (result[0] != null) { + simResult.groups = result[0]; + } + if (result[1] != null) { + simResult.users = result[1]; + } + } catch (e) { + simResult.groups = null; + simResult.users = null; + reject(e || i18nService.t("syncError")); + return; + } - const userMap = new Map(); - this.sortEntries(simResult.users, i18nService); - for (const u of simResult.users) { - userMap.set(u.externalId, u); - if (u.deleted) { - simResult.deletedUsers.push(u); - } else if (u.disabled) { - simResult.disabledUsers.push(u); - } else { - simResult.enabledUsers.push(u); - } - } + const userMap = new Map(); + this.sortEntries(simResult.users, i18nService); + for (const u of simResult.users) { + userMap.set(u.externalId, u); + if (u.deleted) { + simResult.deletedUsers.push(u); + } else if (u.disabled) { + simResult.disabledUsers.push(u); + } else { + simResult.enabledUsers.push(u); + } + } - this.sortEntries(simResult.groups, i18nService); - for (const g of simResult.groups) { - if (g.userMemberExternalIds == null) { - continue; - } - - const anyG = (g as any); - anyG.users = []; - for (const uid of g.userMemberExternalIds) { - if (userMap.has(uid)) { - anyG.users.push(userMap.get(uid)); - } else { - anyG.users.push({ displayName: uid }); - } - } - - this.sortEntries(anyG.users, i18nService); - } - - resolve(simResult); - }); - } - - static adjustConfigForSave(ldap: LdapConfiguration, sync: SyncConfiguration) { - if (ldap.ad) { - sync.creationDateAttribute = 'whenCreated'; - sync.revisionDateAttribute = 'whenChanged'; - sync.emailPrefixAttribute = 'sAMAccountName'; - sync.memberAttribute = 'member'; - sync.userObjectClass = 'person'; - sync.groupObjectClass = 'group'; - sync.userEmailAttribute = 'mail'; - sync.groupNameAttribute = 'name'; - - if (sync.groupPath == null) { - sync.groupPath = 'CN=Users'; - } - if (sync.userPath == null) { - sync.userPath = 'CN=Users'; - } + this.sortEntries(simResult.groups, i18nService); + for (const g of simResult.groups) { + if (g.userMemberExternalIds == null) { + continue; } - if (sync.interval != null) { - if (sync.interval <= 0) { - sync.interval = null; - } else if (sync.interval < 5) { - sync.interval = 5; - } + const anyG = g as any; + anyG.users = []; + for (const uid of g.userMemberExternalIds) { + if (userMap.has(uid)) { + anyG.users.push(userMap.get(uid)); + } else { + anyG.users.push({ displayName: uid }); + } } + + this.sortEntries(anyG.users, i18nService); + } + + resolve(simResult); + }); + } + + static adjustConfigForSave(ldap: LdapConfiguration, sync: SyncConfiguration) { + if (ldap.ad) { + sync.creationDateAttribute = "whenCreated"; + sync.revisionDateAttribute = "whenChanged"; + sync.emailPrefixAttribute = "sAMAccountName"; + sync.memberAttribute = "member"; + sync.userObjectClass = "person"; + sync.groupObjectClass = "group"; + sync.userEmailAttribute = "mail"; + sync.groupNameAttribute = "name"; + + if (sync.groupPath == null) { + sync.groupPath = "CN=Users"; + } + if (sync.userPath == null) { + sync.userPath = "CN=Users"; + } } - private static sortEntries(arr: Entry[], i18nService: I18nService) { - arr.sort((a, b) => { - return i18nService.collator ? i18nService.collator.compare(a.displayName, b.displayName) : - a.displayName.localeCompare(b.displayName); - }); + if (sync.interval != null) { + if (sync.interval <= 0) { + sync.interval = null; + } else if (sync.interval < 5) { + sync.interval = 5; + } } + } + + private static sortEntries(arr: Entry[], i18nService: I18nService) { + arr.sort((a, b) => { + return i18nService.collator + ? i18nService.collator.compare(a.displayName, b.displayName) + : a.displayName.localeCompare(b.displayName); + }); + } } diff --git a/tsconfig.json b/tsconfig.json index d9650c9b..9b76df48 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,28 +12,15 @@ "types": [], "baseUrl": ".", "paths": { - "tldjs": [ - "jslib/src/misc/tldjs.noop" - ], - "jslib-common/*": [ - "jslib/common/src/*" - ], - "jslib-angular/*": [ - "jslib/angular/src/*" - ], - "jslib-electron/*": [ - "jslib/electron/src/*" - ], - "jslib-node/*": [ - "jslib/node/src/*" - ] + "tldjs": ["jslib/src/misc/tldjs.noop"], + "jslib-common/*": ["jslib/common/src/*"], + "jslib-angular/*": ["jslib/angular/src/*"], + "jslib-electron/*": ["jslib/electron/src/*"], + "jslib-node/*": ["jslib/node/src/*"] } }, "angularCompilerOptions": { "preserveWhitespaces": true }, - "include": [ - "src", - "src-cli" - ] + "include": ["src", "src-cli"] } diff --git a/tslint.json b/tslint.json index f23a829a..52ae5030 100644 --- a/tslint.json +++ b/tslint.json @@ -1,17 +1,17 @@ { "extends": "tslint:recommended", "rules": { - "align": [ true, "statements", "members" ], + "align": [true, "statements", "members"], "ban-types": { "options": [ - [ "Object", "Avoid using the `Object` type. Did you mean `object`?" ], - [ "Boolean", "Avoid using the `Boolean` type. Did you mean `boolean`?" ], - [ "Number", "Avoid using the `Number` type. Did you mean `number`?" ], - [ "String", "Avoid using the `String` type. Did you mean `string`?" ], - [ "Symbol", "Avoid using the `Symbol` type. Did you mean `symbol`?" ] + ["Object", "Avoid using the `Object` type. Did you mean `object`?"], + ["Boolean", "Avoid using the `Boolean` type. Did you mean `boolean`?"], + ["Number", "Avoid using the `Number` type. Did you mean `number`?"], + ["String", "Avoid using the `String` type. Did you mean `string`?"], + ["Symbol", "Avoid using the `Symbol` type. Did you mean `symbol`?"] ] }, - "member-access": [ true, "no-public" ], + "member-access": [true, "no-public"], "member-ordering": [ true, { @@ -34,11 +34,10 @@ ] } ], - "no-empty": [ true ], + "no-empty": [true], "object-literal-sort-keys": false, - "object-literal-shorthand": [ true, "never" ], + "object-literal-shorthand": [true, "never"], "prefer-for-of": false, - "quotemark": [ true, "single" ], "whitespace": [ true, "check-branch", @@ -51,7 +50,7 @@ ], "max-classes-per-file": false, "ordered-imports": true, - "arrow-parens": [ true ], + "arrow-parens": [true], "trailing-comma": [ true, { diff --git a/webpack.cli.js b/webpack.cli.js index e3fe360c..dc7fe70b 100644 --- a/webpack.cli.js +++ b/webpack.cli.js @@ -1,79 +1,77 @@ -const path = require('path'); -const webpack = require('webpack'); -const { CleanWebpackPlugin } = require('clean-webpack-plugin'); -const CopyWebpackPlugin = require('copy-webpack-plugin'); -const nodeExternals = require('webpack-node-externals'); -const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); +const path = require("path"); +const webpack = require("webpack"); +const { CleanWebpackPlugin } = require("clean-webpack-plugin"); +const CopyWebpackPlugin = require("copy-webpack-plugin"); +const nodeExternals = require("webpack-node-externals"); +const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin"); if (process.env.NODE_ENV == null) { - process.env.NODE_ENV = 'development'; + process.env.NODE_ENV = "development"; } -const ENV = process.env.ENV = process.env.NODE_ENV; +const ENV = (process.env.ENV = process.env.NODE_ENV); const moduleRules = [ - { - test: /\.ts$/, - enforce: 'pre', - loader: 'tslint-loader', - }, - { - test: /\.ts$/, - use: 'ts-loader', - exclude: path.resolve(__dirname, 'node_modules'), - }, - { - test: /\.node$/, - loader: 'node-loader', - }, + { + test: /\.ts$/, + enforce: "pre", + loader: "tslint-loader", + }, + { + test: /\.ts$/, + use: "ts-loader", + exclude: path.resolve(__dirname, "node_modules"), + }, + { + test: /\.node$/, + loader: "node-loader", + }, ]; const plugins = [ - new CleanWebpackPlugin(), - new CopyWebpackPlugin({ - patterns: [ - { from: './src/locales', to: 'locales' }, - ], - }), - new webpack.DefinePlugin({ - 'process.env.BWCLI_ENV': JSON.stringify(ENV), - }), - new webpack.BannerPlugin({ - banner: '#!/usr/bin/env node', - raw: true - }), - new webpack.IgnorePlugin({ - resourceRegExp: /^encoding$/, - contextRegExp: /node-fetch/, - }), + new CleanWebpackPlugin(), + new CopyWebpackPlugin({ + patterns: [{ from: "./src/locales", to: "locales" }], + }), + new webpack.DefinePlugin({ + "process.env.BWCLI_ENV": JSON.stringify(ENV), + }), + new webpack.BannerPlugin({ + banner: "#!/usr/bin/env node", + raw: true, + }), + new webpack.IgnorePlugin({ + resourceRegExp: /^encoding$/, + contextRegExp: /node-fetch/, + }), ]; const config = { - mode: ENV, - target: 'node', - devtool: ENV === 'development' ? 'eval-source-map' : 'source-map', - node: { - __dirname: false, - __filename: false, - }, - entry: { - 'bwdc': './src/bwdc.ts', - }, - optimization: { - minimize: false, - }, - resolve: { - extensions: ['.ts', '.js', '.json'], - plugins: [new TsconfigPathsPlugin({ configFile: './tsconfig.json' })], - symlinks: false, - modules: [path.resolve('node_modules')], - }, - output: { - filename: '[name].js', - path: path.resolve(__dirname, 'build-cli'), - }, - module: { rules: moduleRules }, - plugins: plugins, - externals: [nodeExternals()], + mode: ENV, + target: "node", + devtool: ENV === "development" ? "eval-source-map" : "source-map", + node: { + __dirname: false, + __filename: false, + }, + entry: { + bwdc: "./src/bwdc.ts", + }, + optimization: { + minimize: false, + }, + resolve: { + extensions: [".ts", ".js", ".json"], + plugins: [new TsconfigPathsPlugin({ configFile: "./tsconfig.json" })], + symlinks: false, + modules: [path.resolve("node_modules")], + }, + output: { + filename: "[name].js", + path: path.resolve(__dirname, "build-cli"), + }, + module: { rules: moduleRules }, + plugins: plugins, + externals: [nodeExternals()], }; module.exports = config; diff --git a/webpack.main.js b/webpack.main.js index a20a33af..e26126db 100644 --- a/webpack.main.js +++ b/webpack.main.js @@ -1,68 +1,68 @@ -const path = require('path'); -const { merge } = require('webpack-merge'); -const CopyWebpackPlugin = require('copy-webpack-plugin'); -const { CleanWebpackPlugin } = require('clean-webpack-plugin'); -const nodeExternals = require('webpack-node-externals'); -const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); +const path = require("path"); +const { merge } = require("webpack-merge"); +const CopyWebpackPlugin = require("copy-webpack-plugin"); +const { CleanWebpackPlugin } = require("clean-webpack-plugin"); +const nodeExternals = require("webpack-node-externals"); +const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin"); const common = { - module: { - rules: [ - { - test: /\.ts$/, - enforce: 'pre', - loader: 'tslint-loader', - }, - { - test: /\.tsx?$/, - use: 'ts-loader', - exclude: /node_modules\/(?!(@bitwarden)\/).*/, - }, - ], - }, - plugins: [], - resolve: { - extensions: ['.tsx', '.ts', '.js'], - plugins: [new TsconfigPathsPlugin({ configFile: './tsconfig.json' })], - }, - output: { - filename: '[name].js', - path: path.resolve(__dirname, 'build'), - }, + module: { + rules: [ + { + test: /\.ts$/, + enforce: "pre", + loader: "tslint-loader", + }, + { + test: /\.tsx?$/, + use: "ts-loader", + exclude: /node_modules\/(?!(@bitwarden)\/).*/, + }, + ], + }, + plugins: [], + resolve: { + extensions: [".tsx", ".ts", ".js"], + plugins: [new TsconfigPathsPlugin({ configFile: "./tsconfig.json" })], + }, + output: { + filename: "[name].js", + path: path.resolve(__dirname, "build"), + }, }; const main = { - mode: 'production', - target: 'electron-main', - node: { - __dirname: false, - __filename: false, - }, - entry: { - 'main': './src/main.ts', - }, - optimization: { - minimize: false, - }, - module: { - rules: [ - { - test: /\.node$/, - loader: 'node-loader', - }, - ], - }, - plugins: [ - new CleanWebpackPlugin(), - new CopyWebpackPlugin({ - patterns: [ - './src/package.json', - { from: './src/images', to: 'images' }, - { from: './src/locales', to: 'locales' }, - ], - }), + mode: "production", + target: "electron-main", + node: { + __dirname: false, + __filename: false, + }, + entry: { + main: "./src/main.ts", + }, + optimization: { + minimize: false, + }, + module: { + rules: [ + { + test: /\.node$/, + loader: "node-loader", + }, ], - externals: [nodeExternals()], + }, + plugins: [ + new CleanWebpackPlugin(), + new CopyWebpackPlugin({ + patterns: [ + "./src/package.json", + { from: "./src/images", to: "images" }, + { from: "./src/locales", to: "locales" }, + ], + }), + ], + externals: [nodeExternals()], }; module.exports = merge(common, main); diff --git a/webpack.renderer.js b/webpack.renderer.js index 35158e71..b911705a 100644 --- a/webpack.renderer.js +++ b/webpack.renderer.js @@ -1,127 +1,129 @@ -const path = require('path'); -const webpack = require('webpack'); -const { merge } = require('webpack-merge'); -const HtmlWebpackPlugin = require('html-webpack-plugin'); -const MiniCssExtractPlugin = require('mini-css-extract-plugin'); -const { AngularWebpackPlugin } = require('@ngtools/webpack'); -const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); +const path = require("path"); +const webpack = require("webpack"); +const { merge } = require("webpack-merge"); +const HtmlWebpackPlugin = require("html-webpack-plugin"); +const MiniCssExtractPlugin = require("mini-css-extract-plugin"); +const { AngularWebpackPlugin } = require("@ngtools/webpack"); +const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin"); const common = { - module: { - rules: [ - { - test: /\.ts$/, - enforce: 'pre', - loader: 'tslint-loader', - }, - { - test: /(?:\.ngfactory\.js|\.ngstyle\.js|\.ts)$/, - loader: '@ngtools/webpack', - }, - { - test: /\.(jpe?g|png|gif|svg)$/i, - exclude: /.*(fontawesome-webfont)\.svg/, - generator: { - filename: 'images/[name].[ext]', - }, - type: 'asset/resource', - }, - ], - }, - plugins: [], - resolve: { - extensions: ['.tsx', '.ts', '.js', '.json'], - plugins: [new TsconfigPathsPlugin({ configFile: './tsconfig.json' })], - symlinks: false, - modules: [path.resolve('node_modules')], - }, - output: { - filename: '[name].js', - path: path.resolve(__dirname, 'build'), - }, + module: { + rules: [ + { + test: /\.ts$/, + enforce: "pre", + loader: "tslint-loader", + }, + { + test: /(?:\.ngfactory\.js|\.ngstyle\.js|\.ts)$/, + loader: "@ngtools/webpack", + }, + { + test: /\.(jpe?g|png|gif|svg)$/i, + exclude: /.*(fontawesome-webfont)\.svg/, + generator: { + filename: "images/[name].[ext]", + }, + type: "asset/resource", + }, + ], + }, + plugins: [], + resolve: { + extensions: [".tsx", ".ts", ".js", ".json"], + plugins: [new TsconfigPathsPlugin({ configFile: "./tsconfig.json" })], + symlinks: false, + modules: [path.resolve("node_modules")], + }, + output: { + filename: "[name].js", + path: path.resolve(__dirname, "build"), + }, }; const renderer = { - mode: 'production', - devtool: false, - target: 'electron-renderer', - node: { - __dirname: false, - }, - entry: { - 'app/main': './src/app/main.ts', - }, - optimization: { - minimize: false, - splitChunks: { - cacheGroups: { - commons: { - test: /[\\/]node_modules[\\/]/, - name: 'app/vendor', - chunks: (chunk) => { - return chunk.name === 'app/main'; - }, - }, - }, + mode: "production", + devtool: false, + target: "electron-renderer", + node: { + __dirname: false, + }, + entry: { + "app/main": "./src/app/main.ts", + }, + optimization: { + minimize: false, + splitChunks: { + cacheGroups: { + commons: { + test: /[\\/]node_modules[\\/]/, + name: "app/vendor", + chunks: (chunk) => { + return chunk.name === "app/main"; + }, }, + }, }, - module: { - rules: [ - { - test: /\.(html)$/, - loader: 'html-loader', - }, - { - test: /.(ttf|otf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/, - exclude: /loading.svg/, - generator: { - filename: 'fonts/[name].[ext]', - }, - type: 'asset/resource', - }, - { - test: /\.scss$/, - use: [ - { - loader: MiniCssExtractPlugin.loader, - options: { - publicPath: '../', - }, - }, - 'css-loader', - 'sass-loader', - ], - }, - // Hide System.import warnings. ref: https://github.com/angular/angular/issues/21560 - { - test: /[\/\\]@angular[\/\\].+\.js$/, - parser: { system: true }, + }, + module: { + rules: [ + { + test: /\.(html)$/, + loader: "html-loader", + }, + { + test: /.(ttf|otf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/, + exclude: /loading.svg/, + generator: { + filename: "fonts/[name].[ext]", + }, + type: "asset/resource", + }, + { + test: /\.scss$/, + use: [ + { + loader: MiniCssExtractPlugin.loader, + options: { + publicPath: "../", }, + }, + "css-loader", + "sass-loader", ], - }, - plugins: [ - new AngularWebpackPlugin({ - tsConfigPath: 'tsconfig.json', - entryModule: 'src/app/app.module#AppModule', - sourceMap: true, - }), - // ref: https://github.com/angular/angular/issues/20357 - new webpack.ContextReplacementPlugin(/\@angular(\\|\/)core(\\|\/)fesm5/, - path.resolve(__dirname, './src')), - new HtmlWebpackPlugin({ - template: './src/index.html', - filename: 'index.html', - chunks: ['app/vendor', 'app/main'], - }), - new webpack.SourceMapDevToolPlugin({ - include: ['app/main.js'], - }), - new webpack.DefinePlugin({ 'global.GENTLY': false }), - new MiniCssExtractPlugin({ - filename: '[name].[contenthash].css', - chunkFilename: '[id].[contenthash].css', - }), + }, + // Hide System.import warnings. ref: https://github.com/angular/angular/issues/21560 + { + test: /[\/\\]@angular[\/\\].+\.js$/, + parser: { system: true }, + }, ], + }, + plugins: [ + new AngularWebpackPlugin({ + tsConfigPath: "tsconfig.json", + entryModule: "src/app/app.module#AppModule", + sourceMap: true, + }), + // ref: https://github.com/angular/angular/issues/20357 + new webpack.ContextReplacementPlugin( + /\@angular(\\|\/)core(\\|\/)fesm5/, + path.resolve(__dirname, "./src") + ), + new HtmlWebpackPlugin({ + template: "./src/index.html", + filename: "index.html", + chunks: ["app/vendor", "app/main"], + }), + new webpack.SourceMapDevToolPlugin({ + include: ["app/main.js"], + }), + new webpack.DefinePlugin({ "global.GENTLY": false }), + new MiniCssExtractPlugin({ + filename: "[name].[contenthash].css", + chunkFilename: "[id].[contenthash].css", + }), + ], }; module.exports = merge(common, renderer);