1
0
mirror of https://github.com/bitwarden/directory-connector synced 2025-12-05 23:53:21 +00:00

Apply Prettier (#194)

This commit is contained in:
Oscar Hinton
2021-12-20 17:14:18 +01:00
committed by GitHub
parent 225073aa33
commit 096196fcd5
76 changed files with 6056 additions and 5208 deletions

View File

@@ -1,4 +1,5 @@
## Type of change ## Type of change
- [ ] Bug fix - [ ] Bug fix
- [ ] New feature development - [ ] New feature development
- [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc) - [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc)
@@ -6,27 +7,26 @@
- [ ] Other - [ ] Other
## Objective ## Objective
<!--Describe what the purpose of this PR is. For example: what bug you're fixing or what new feature you're adding--> <!--Describe what the purpose of this PR is. For example: what bug you're fixing or what new feature you're adding-->
## Code changes ## Code changes
<!--Explain the changes you've made to each file or major component. This should help the reviewer understand your changes--> <!--Explain the changes you've made to each file or major component. This should help the reviewer understand your changes-->
<!--Also refer to any related changes or PRs in other repositories--> <!--Also refer to any related changes or PRs in other repositories-->
* **file.ext:** Description of what was changed and why - **file.ext:** Description of what was changed and why
## Screenshots ## Screenshots
<!--Required for any UI changes. Delete if not applicable--> <!--Required for any UI changes. Delete if not applicable-->
## Testing requirements ## Testing requirements
<!--What functionality requires testing by QA? This includes testing new behavior and regression testing--> <!--What functionality requires testing by QA? This includes testing new behavior and regression testing-->
## Before you submit ## Before you submit
- [ ] I have checked for **linting** errors (`npm run lint`) (required) - [ ] 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) - [ ] 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) - [ ] This change requires a **documentation update** (notify the documentation team)

66
.vscode/launch.json vendored
View File

@@ -1,48 +1,40 @@
{ {
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"type": "node", "type": "node",
"request": "launch", "request": "launch",
"name": "Electron: Main", "name": "Electron: Main",
"protocol": "inspector", "protocol": "inspector",
"cwd": "${workspaceRoot}/build", "cwd": "${workspaceRoot}/build",
"runtimeArgs": [ "runtimeArgs": ["--remote-debugging-port=9223", "."],
"--remote-debugging-port=9223", "windows": {
"." "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
],
"windows": {
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
},
"sourceMaps": true
}, },
{ "sourceMaps": true
"name": "Electron: Renderer",
"type": "chrome",
"request": "attach",
"port": 9223,
"webRoot": "${workspaceFolder}/build",
"sourceMaps": true
}, },
{ {
"type": "node", "name": "Electron: Renderer",
"request": "launch", "type": "chrome",
"name": "Debug CLI", "request": "attach",
"protocol": "inspector", "port": 9223,
"cwd": "${workspaceFolder}", "webRoot": "${workspaceFolder}/build",
"program": "${workspaceFolder}/build-cli/bwdc.js", "sourceMaps": true
"args": [ },
"sync" {
] "type": "node",
"request": "launch",
"name": "Debug CLI",
"protocol": "inspector",
"cwd": "${workspaceFolder}",
"program": "${workspaceFolder}/build-cli/bwdc.js",
"args": ["sync"]
} }
], ],
"compounds": [ "compounds": [
{ {
"name": "Electron: All", "name": "Electron: All",
"configurations": [ "configurations": ["Electron: Main", "Electron: Renderer"]
"Electron: Main", }
"Electron: Renderer"
]
}
] ]
} }

View File

@@ -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. 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: Supported directories:
- Active Directory - Active Directory
- Any other LDAP-based directory - Any other LDAP-based directory
- Azure Active Directory - Azure Active Directory

View File

@@ -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 - 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. 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 - 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 - 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 degradation of our service. Only interact with accounts you own or with explicit permission of the
account holder. account holder.

View File

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

View File

@@ -1,22 +1,19 @@
exports.default = async function(configuration) { exports.default = async function (configuration) {
if ( if (parseInt(process.env.ELECTRON_BUILDER_SIGN) === 1 && configuration.path.slice(-4) == ".exe") {
parseInt(process.env.ELECTRON_BUILDER_SIGN) === 1 && console.log(`[*] Signing file: ${configuration.path}`);
configuration.path.slice(-4) == ".exe"
) {
console.log(`[*] Signing file: ${configuration.path}`)
require("child_process").execSync( require("child_process").execSync(
`azuresigntool sign ` + `azuresigntool sign ` +
`-kvu ${process.env.SIGNING_VAULT_URL} ` + `-kvu ${process.env.SIGNING_VAULT_URL} ` +
`-kvi ${process.env.SIGNING_CLIENT_ID} ` + `-kvi ${process.env.SIGNING_CLIENT_ID} ` +
`-kvt ${process.env.SIGNING_TENANT_ID} ` + `-kvt ${process.env.SIGNING_TENANT_ID} ` +
`-kvs ${process.env.SIGNING_CLIENT_SECRET} ` + `-kvs ${process.env.SIGNING_CLIENT_SECRET} ` +
`-kvc ${process.env.SIGNING_CERT_NAME} ` + `-kvc ${process.env.SIGNING_CERT_NAME} ` +
`-fd ${configuration.hash} ` + `-fd ${configuration.hash} ` +
`-du ${configuration.site} ` + `-du ${configuration.site} ` +
`-tr http://timestamp.digicert.com ` + `-tr http://timestamp.digicert.com ` +
`"${configuration.path}"`, `"${configuration.path}"`,
{ {
stdio: "inherit" stdio: "inherit",
} }
); );
} }

View File

@@ -1,47 +1,60 @@
<div class="container-fluid"> <div class="container-fluid">
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise"> <form #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-8 col-lg-6"> <div class="col-md-8 col-lg-6">
<p class="text-center font-weight-bold">{{'welcome' | i18n}}</p> <p class="text-center font-weight-bold">{{ "welcome" | i18n }}</p>
<p class="text-center">{{'logInDesc' | i18n}}</p> <p class="text-center">{{ "logInDesc" | i18n }}</p>
<div class="card"> <div class="card">
<h5 class="card-header">{{'logIn' | i18n}}</h5> <h5 class="card-header">{{ "logIn" | i18n }}</h5>
<div class="card-body"> <div class="card-body">
<div class="form-group"> <div class="form-group">
<label for="client_id">{{'clientId' | i18n}}</label> <label for="client_id">{{ "clientId" | i18n }}</label>
<input id="client_id" name="ClientId" [(ngModel)]="clientId" <input id="client_id" name="ClientId" [(ngModel)]="clientId" class="form-control" />
class="form-control">
</div>
<div class="form-group">
<div class="row-main">
<label for="client_secret">{{'clientSecret' | i18n}}</label>
<div class="input-group">
<input type="{{showSecret ? 'text' : 'password'}}" id="client_secret" name="ClientSecret"
[(ngModel)]="clientSecret" class="form-control">
<div class="input-group-append">
<button type="button" class="ml-1 btn btn-link" appA11yTitle="{{'toggleVisibility' | i18n}}" (click)="toggleSecret()">
<i class="fa fa-lg" aria-hidden="true"[ngClass]="showSecret ? 'fa-eye-slash' : 'fa-eye'"></i>
</button>
</div>
</div>
</div>
</div>
<div class="d-flex">
<div>
<button type="submit" class="btn btn-primary" [disabled]="form.loading">
<i class="fa fa-spinner fa-fw fa-spin" [hidden]="!form.loading"></i>
<i class="fa fa-sign-in fa-fw" [hidden]="form.loading"></i>
{{'logIn' | i18n}}
</button>
</div>
<button type="button" class="btn btn-link ml-auto" (click)="settings()">
{{'settings' | i18n}}
</button>
</div>
</div>
</div>
</div> </div>
<div class="form-group">
<div class="row-main">
<label for="client_secret">{{ "clientSecret" | i18n }}</label>
<div class="input-group">
<input
type="{{ showSecret ? 'text' : 'password' }}"
id="client_secret"
name="ClientSecret"
[(ngModel)]="clientSecret"
class="form-control"
/>
<div class="input-group-append">
<button
type="button"
class="ml-1 btn btn-link"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="toggleSecret()"
>
<i
class="fa fa-lg"
aria-hidden="true"
[ngClass]="showSecret ? 'fa-eye-slash' : 'fa-eye'"
></i>
</button>
</div>
</div>
</div>
</div>
<div class="d-flex">
<div>
<button type="submit" class="btn btn-primary" [disabled]="form.loading">
<i class="fa fa-spinner fa-fw fa-spin" [hidden]="!form.loading"></i>
<i class="fa fa-sign-in fa-fw" [hidden]="form.loading"></i>
{{ "logIn" | i18n }}
</button>
</div>
<button type="button" class="btn btn-link ml-auto" (click)="settings()">
{{ "settings" | i18n }}
</button>
</div>
</div>
</div> </div>
</form> </div>
</div>
</form>
</div> </div>
<ng-template #environment></ng-template> <ng-template #environment></ng-template>

View File

@@ -1,87 +1,110 @@
import { import {
Component, Component,
ComponentFactoryResolver, ComponentFactoryResolver,
Input, Input,
ViewChild, ViewChild,
ViewContainerRef, ViewContainerRef,
} from '@angular/core'; } from "@angular/core";
import { Router } from '@angular/router'; import { Router } from "@angular/router";
import { EnvironmentComponent } from './environment.component'; import { EnvironmentComponent } from "./environment.component";
import { ApiKeyService } from 'jslib-common/abstractions/apiKey.service'; import { ApiKeyService } from "jslib-common/abstractions/apiKey.service";
import { AuthService } from 'jslib-common/abstractions/auth.service'; import { AuthService } from "jslib-common/abstractions/auth.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from 'jslib-common/abstractions/log.service'; import { LogService } from "jslib-common/abstractions/log.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.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 { Utils } from "jslib-common/misc/utils";
import { ConfigurationService } from '../../services/configuration.service'; import { ConfigurationService } from "../../services/configuration.service";
@Component({ @Component({
selector: 'app-apiKey', selector: "app-apiKey",
templateUrl: 'apiKey.component.html', templateUrl: "apiKey.component.html",
}) })
export class ApiKeyComponent { export class ApiKeyComponent {
@ViewChild('environment', { read: ViewContainerRef, static: true }) environmentModal: ViewContainerRef; @ViewChild("environment", { read: ViewContainerRef, static: true })
@Input() clientId: string = ''; environmentModal: ViewContainerRef;
@Input() clientSecret: string = ''; @Input() clientId: string = "";
@Input() clientSecret: string = "";
formPromise: Promise<any>; formPromise: Promise<any>;
successRoute = '/tabs/dashboard'; successRoute = "/tabs/dashboard";
showSecret: boolean = false; showSecret: boolean = false;
constructor(private authService: AuthService, private apiKeyService: ApiKeyService, private router: Router, constructor(
private i18nService: I18nService, private componentFactoryResolver: ComponentFactoryResolver, private authService: AuthService,
private configurationService: ConfigurationService, private platformUtilsService: PlatformUtilsService, private apiKeyService: ApiKeyService,
private modalService: ModalService, private logService: LogService) { } private router: Router,
private i18nService: I18nService,
private componentFactoryResolver: ComponentFactoryResolver,
private configurationService: ConfigurationService,
private platformUtilsService: PlatformUtilsService,
private modalService: ModalService,
private logService: LogService
) {}
async submit() { async submit() {
if (this.clientId == null || this.clientId === '') { if (this.clientId == null || this.clientId === "") {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), this.platformUtilsService.showToast(
this.i18nService.t('clientIdRequired')); "error",
return; this.i18nService.t("errorOccurred"),
} this.i18nService.t("clientIdRequired")
if (!this.clientId.startsWith('organization')) { );
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), return;
this.i18nService.t('orgApiKeyRequired')); }
return; if (!this.clientId.startsWith("organization")) {
} this.platformUtilsService.showToast(
if (this.clientSecret == null || this.clientSecret === '') { "error",
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), this.i18nService.t("errorOccurred"),
this.i18nService.t('clientSecretRequired')); this.i18nService.t("orgApiKeyRequired")
return; );
} return;
const idParts = this.clientId.split('.'); }
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])) { if (idParts.length !== 2 || idParts[0] !== "organization" || !Utils.isGuid(idParts[1])) {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), this.platformUtilsService.showToast(
this.i18nService.t('invalidClientId')); "error",
return; this.i18nService.t("errorOccurred"),
} this.i18nService.t("invalidClientId")
);
try { return;
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);
}
} }
async settings() { try {
const [modalRef, childComponent] = await this.modalService.openViewRef(EnvironmentComponent, this.environmentModal); 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(() => { async settings() {
modalRef.close(); const [modalRef, childComponent] = await this.modalService.openViewRef(
}); EnvironmentComponent,
} this.environmentModal
toggleSecret() { );
this.showSecret = !this.showSecret;
document.getElementById('client_secret').focus(); childComponent.onSaved.subscribe(() => {
} modalRef.close();
});
}
toggleSecret() {
this.showSecret = !this.showSecret;
document.getElementById("client_secret").focus();
}
} }

View File

@@ -1,43 +1,61 @@
<div class="modal fade"> <div class="modal fade">
<div class="modal-dialog"> <div class="modal-dialog">
<form class="modal-content" (ngSubmit)="submit()"> <form class="modal-content" (ngSubmit)="submit()">
<div class="modal-header"> <div class="modal-header">
<h3 class="modal-title">{{'settings' | i18n}}</h3> <h3 class="modal-title">{{ "settings" | i18n }}</h3>
<button type="button" class="close" data-dismiss="modal" title="Close"> <button type="button" class="close" data-dismiss="modal" title="Close">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<h4>{{'selfHostedEnvironment' | i18n}}</h4> <h4>{{ "selfHostedEnvironment" | i18n }}</h4>
<p>{{'selfHostedEnvironmentFooter' | i18n}}</p> <p>{{ "selfHostedEnvironmentFooter" | i18n }}</p>
<div class="form-group"> <div class="form-group">
<label for="baseUrl">{{'baseUrl' | i18n}}</label> <label for="baseUrl">{{ "baseUrl" | i18n }}</label>
<input id="baseUrl" type="text" name="BaseUrl" [(ngModel)]="baseUrl" class="form-control"> <input
<small class="text-muted form-text">{{'ex' | i18n}} https://bitwarden.company.com</small> id="baseUrl"
</div> type="text"
<h4>{{'customEnvironment' | i18n}}</h4> name="BaseUrl"
<p>{{'customEnvironmentFooter' | i18n}}</p> [(ngModel)]="baseUrl"
<div class="form-group"> class="form-control"
<label for="webVaultUrl">{{'webVaultUrl' | i18n}}</label> />
<input id="webVaultUrl" type="text" name="WebVaultUrl" [(ngModel)]="webVaultUrl" <small class="text-muted form-text"
class="form-control"> >{{ "ex" | i18n }} https://bitwarden.company.com</small
</div> >
<div class="form-group"> </div>
<label for="apiUrl">{{'apiUrl' | i18n}}</label> <h4>{{ "customEnvironment" | i18n }}</h4>
<input id="apiUrl" type="text" name="ApiUrl" [(ngModel)]="apiUrl" class="form-control"> <p>{{ "customEnvironmentFooter" | i18n }}</p>
</div> <div class="form-group">
<div class="form-group"> <label for="webVaultUrl">{{ "webVaultUrl" | i18n }}</label>
<label for="identityUrl">{{'identityUrl' | i18n}}</label> <input
<input id="identityUrl" type="text" name="IdentityUrl" [(ngModel)]="identityUrl" id="webVaultUrl"
class="form-control"> type="text"
</div> name="WebVaultUrl"
</div> [(ngModel)]="webVaultUrl"
<div class="modal-footer justify-content-start"> class="form-control"
<button type="submit" class="btn btn-primary"> />
<i class="fa fa-save fa-fw"></i> </div>
{{'save' | i18n}} <div class="form-group">
</button> <label for="apiUrl">{{ "apiUrl" | i18n }}</label>
</div> <input id="apiUrl" type="text" name="ApiUrl" [(ngModel)]="apiUrl" class="form-control" />
</form> </div>
</div> <div class="form-group">
<label for="identityUrl">{{ "identityUrl" | i18n }}</label>
<input
id="identityUrl"
type="text"
name="IdentityUrl"
[(ngModel)]="identityUrl"
class="form-control"
/>
</div>
</div>
<div class="modal-footer justify-content-start">
<button type="submit" class="btn btn-primary">
<i class="fa fa-save fa-fw"></i>
{{ "save" | i18n }}
</button>
</div>
</form>
</div>
</div> </div>

View File

@@ -1,18 +1,21 @@
import { Component } from '@angular/core'; import { Component } from "@angular/core";
import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.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({ @Component({
selector: 'app-environment', selector: "app-environment",
templateUrl: 'environment.component.html', templateUrl: "environment.component.html",
}) })
export class EnvironmentComponent extends BaseEnvironmentComponent { export class EnvironmentComponent extends BaseEnvironmentComponent {
constructor(environmentService: EnvironmentService, i18nService: I18nService, constructor(
platformUtilsService: PlatformUtilsService) { environmentService: EnvironmentService,
super(platformUtilsService, environmentService, i18nService); i18nService: I18nService,
} platformUtilsService: PlatformUtilsService
) {
super(platformUtilsService, environmentService, i18nService);
}
} }

View File

@@ -1,58 +1,57 @@
import { NgModule } from '@angular/core'; import { NgModule } from "@angular/core";
import { import { RouterModule, Routes } from "@angular/router";
RouterModule,
Routes,
} from '@angular/router';
import { AuthGuardService } from './services/auth-guard.service'; import { AuthGuardService } from "./services/auth-guard.service";
import { LaunchGuardService } from './services/launch-guard.service'; import { LaunchGuardService } from "./services/launch-guard.service";
import { ApiKeyComponent } from './accounts/apiKey.component'; import { ApiKeyComponent } from "./accounts/apiKey.component";
import { DashboardComponent } from './tabs/dashboard.component'; import { DashboardComponent } from "./tabs/dashboard.component";
import { MoreComponent } from './tabs/more.component'; import { MoreComponent } from "./tabs/more.component";
import { SettingsComponent } from './tabs/settings.component'; import { SettingsComponent } from "./tabs/settings.component";
import { TabsComponent } from './tabs/tabs.component'; import { TabsComponent } from "./tabs/tabs.component";
const routes: Routes = [ const routes: Routes = [
{ path: '', redirectTo: '/login', pathMatch: 'full' }, { path: "", redirectTo: "/login", pathMatch: "full" },
{ {
path: 'login', path: "login",
component: ApiKeyComponent, component: ApiKeyComponent,
canActivate: [LaunchGuardService], canActivate: [LaunchGuardService],
}, },
{ {
path: 'tabs', path: "tabs",
component: TabsComponent, component: TabsComponent,
children: [ children: [
{ {
path: '', path: "",
redirectTo: '/tabs/dashboard', redirectTo: "/tabs/dashboard",
pathMatch: 'full', pathMatch: "full",
}, },
{ {
path: 'dashboard', path: "dashboard",
component: DashboardComponent, component: DashboardComponent,
canActivate: [AuthGuardService], canActivate: [AuthGuardService],
}, },
{ {
path: 'settings', path: "settings",
component: SettingsComponent, component: SettingsComponent,
canActivate: [AuthGuardService], canActivate: [AuthGuardService],
}, },
{ {
path: 'more', path: "more",
component: MoreComponent, component: MoreComponent,
canActivate: [AuthGuardService], canActivate: [AuthGuardService],
}, },
], ],
}, },
]; ];
@NgModule({ @NgModule({
imports: [RouterModule.forRoot(routes, { imports: [
useHash: true, RouterModule.forRoot(routes, {
/*enableTracing: true,*/ useHash: true,
})], /*enableTracing: true,*/
exports: [RouterModule], }),
],
exports: [RouterModule],
}) })
export class AppRoutingModule { } export class AppRoutingModule {}

View File

@@ -1,155 +1,166 @@
import { import {
Component, Component,
NgZone, NgZone,
OnInit, OnInit,
SecurityContext, SecurityContext,
ViewChild, ViewChild,
ViewContainerRef, ViewContainerRef,
} from '@angular/core'; } from "@angular/core";
import { DomSanitizer } from '@angular/platform-browser'; import { DomSanitizer } from "@angular/platform-browser";
import { Router } from '@angular/router'; import { Router } from "@angular/router";
import { import { IndividualConfig, ToastrService } from "ngx-toastr";
IndividualConfig,
ToastrService,
} from 'ngx-toastr';
import { AuthService } from 'jslib-common/abstractions/auth.service'; import { AuthService } from "jslib-common/abstractions/auth.service";
import { BroadcasterService } from 'jslib-common/abstractions/broadcaster.service'; import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from 'jslib-common/abstractions/log.service'; import { LogService } from "jslib-common/abstractions/log.service";
import { MessagingService } from 'jslib-common/abstractions/messaging.service'; import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { StateService } from 'jslib-common/abstractions/state.service'; import { StateService } from "jslib-common/abstractions/state.service";
import { TokenService } from 'jslib-common/abstractions/token.service'; import { TokenService } from "jslib-common/abstractions/token.service";
import { UserService } from 'jslib-common/abstractions/user.service'; import { UserService } from "jslib-common/abstractions/user.service";
import { ConfigurationService } from '../services/configuration.service'; import { ConfigurationService } from "../services/configuration.service";
import { SyncService } from '../services/sync.service'; import { SyncService } from "../services/sync.service";
const BroadcasterSubscriptionId = 'AppComponent'; const BroadcasterSubscriptionId = "AppComponent";
@Component({ @Component({
selector: 'app-root', selector: "app-root",
styles: [], styles: [],
template: ` template: ` <ng-template #settings></ng-template>
<ng-template #settings></ng-template> <router-outlet></router-outlet>`,
<router-outlet></router-outlet>`,
}) })
export class AppComponent implements OnInit { 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, constructor(
private tokenService: TokenService, private broadcasterService: BroadcasterService,
private authService: AuthService, private router: Router, private userService: UserService,
private toastrService: ToastrService, private i18nService: I18nService, private tokenService: TokenService,
private sanitizer: DomSanitizer, private ngZone: NgZone, private authService: AuthService,
private platformUtilsService: PlatformUtilsService, private messagingService: MessagingService, private router: Router,
private configurationService: ConfigurationService, private syncService: SyncService, private toastrService: ToastrService,
private stateService: StateService, private logService: LogService) { 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() { ngOnInit() {
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => { this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
this.ngZone.run(async () => { this.ngZone.run(async () => {
switch (message.command) { switch (message.command) {
case 'syncScheduleStarted': case "syncScheduleStarted":
case 'syncScheduleStopped': case "syncScheduleStopped":
this.stateService.save('syncingDir', message.command === 'syncScheduleStarted'); this.stateService.save("syncingDir", message.command === "syncScheduleStarted");
break; break;
case 'logout': case "logout":
this.logOut(!!message.expired); this.logOut(!!message.expired);
break; break;
case 'checkDirSync': case "checkDirSync":
try { try {
const syncConfig = await this.configurationService.getSync(); const syncConfig = await this.configurationService.getSync();
if (syncConfig.interval == null || syncConfig.interval < 5) { if (syncConfig.interval == null || syncConfig.interval < 5) {
return; return;
} }
const syncInterval = syncConfig.interval * 60000; const syncInterval = syncConfig.interval * 60000;
const lastGroupSync = await this.configurationService.getLastGroupSyncDate(); const lastGroupSync = await this.configurationService.getLastGroupSyncDate();
const lastUserSync = await this.configurationService.getLastUserSyncDate(); const lastUserSync = await this.configurationService.getLastUserSyncDate();
let lastSync: Date = null; let lastSync: Date = null;
if (lastGroupSync != null && lastUserSync == null) { if (lastGroupSync != null && lastUserSync == null) {
lastSync = lastGroupSync; lastSync = lastGroupSync;
} else if (lastGroupSync == null && lastUserSync != null) { } else if (lastGroupSync == null && lastUserSync != null) {
lastSync = lastUserSync; lastSync = lastUserSync;
} else if (lastGroupSync != null && lastUserSync != null) { } else if (lastGroupSync != null && lastUserSync != null) {
if (lastGroupSync.getTime() < lastUserSync.getTime()) { if (lastGroupSync.getTime() < lastUserSync.getTime()) {
lastSync = lastGroupSync; lastSync = lastGroupSync;
} else { } else {
lastSync = lastUserSync; 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:
} }
}
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:
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<IndividualConfig> = {};
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 += ('<p>' + this.sanitizer.sanitize(SecurityContext.HTML, t) + '</p>'));
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); 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<IndividualConfig> = {};
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 += "<p>" + this.sanitizer.sanitize(SecurityContext.HTML, t) + "</p>")
);
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);
}
} }

View File

@@ -1,74 +1,74 @@
import 'core-js/stable'; import "core-js/stable";
import 'zone.js/dist/zone'; import "zone.js/dist/zone";
import { AppRoutingModule } from './app-routing.module'; import { AppRoutingModule } from "./app-routing.module";
import { ServicesModule } from './services/services.module'; import { ServicesModule } from "./services/services.module";
import { NgModule } from '@angular/core'; import { NgModule } from "@angular/core";
import { FormsModule } from '@angular/forms'; import { FormsModule } from "@angular/forms";
import { BrowserModule } from '@angular/platform-browser'; import { BrowserModule } from "@angular/platform-browser";
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 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 { CalloutComponent } from "jslib-angular/components/callout.component";
import { IconComponent } from 'jslib-angular/components/icon.component'; import { IconComponent } from "jslib-angular/components/icon.component";
import { BitwardenToastModule } from 'jslib-angular/components/toastr.component'; import { BitwardenToastModule } from "jslib-angular/components/toastr.component";
import { ApiKeyComponent } from './accounts/apiKey.component'; import { ApiKeyComponent } from "./accounts/apiKey.component";
import { EnvironmentComponent } from './accounts/environment.component'; import { EnvironmentComponent } from "./accounts/environment.component";
import { DashboardComponent } from './tabs/dashboard.component'; import { DashboardComponent } from "./tabs/dashboard.component";
import { MoreComponent } from './tabs/more.component'; import { MoreComponent } from "./tabs/more.component";
import { SettingsComponent } from './tabs/settings.component'; import { SettingsComponent } from "./tabs/settings.component";
import { TabsComponent } from './tabs/tabs.component'; import { TabsComponent } from "./tabs/tabs.component";
import { A11yTitleDirective } from 'jslib-angular/directives/a11y-title.directive'; import { A11yTitleDirective } from "jslib-angular/directives/a11y-title.directive";
import { ApiActionDirective } from 'jslib-angular/directives/api-action.directive'; import { ApiActionDirective } from "jslib-angular/directives/api-action.directive";
import { AutofocusDirective } from 'jslib-angular/directives/autofocus.directive'; import { AutofocusDirective } from "jslib-angular/directives/autofocus.directive";
import { BlurClickDirective } from 'jslib-angular/directives/blur-click.directive'; import { BlurClickDirective } from "jslib-angular/directives/blur-click.directive";
import { BoxRowDirective } from 'jslib-angular/directives/box-row.directive'; import { BoxRowDirective } from "jslib-angular/directives/box-row.directive";
import { FallbackSrcDirective } from 'jslib-angular/directives/fallback-src.directive'; import { FallbackSrcDirective } from "jslib-angular/directives/fallback-src.directive";
import { StopClickDirective } from 'jslib-angular/directives/stop-click.directive'; import { StopClickDirective } from "jslib-angular/directives/stop-click.directive";
import { StopPropDirective } from 'jslib-angular/directives/stop-prop.directive'; import { StopPropDirective } from "jslib-angular/directives/stop-prop.directive";
import { I18nPipe } from 'jslib-angular/pipes/i18n.pipe'; import { I18nPipe } from "jslib-angular/pipes/i18n.pipe";
import { SearchCiphersPipe } from 'jslib-angular/pipes/search-ciphers.pipe'; import { SearchCiphersPipe } from "jslib-angular/pipes/search-ciphers.pipe";
@NgModule({ @NgModule({
imports: [ imports: [
BrowserModule, BrowserModule,
BrowserAnimationsModule, BrowserAnimationsModule,
FormsModule, FormsModule,
AppRoutingModule, AppRoutingModule,
ServicesModule, ServicesModule,
BitwardenToastModule.forRoot({ BitwardenToastModule.forRoot({
maxOpened: 5, maxOpened: 5,
autoDismiss: true, autoDismiss: true,
closeButton: true, closeButton: true,
}), }),
], ],
declarations: [ declarations: [
A11yTitleDirective, A11yTitleDirective,
ApiActionDirective, ApiActionDirective,
ApiKeyComponent, ApiKeyComponent,
AppComponent, AppComponent,
AutofocusDirective, AutofocusDirective,
BlurClickDirective, BlurClickDirective,
BoxRowDirective, BoxRowDirective,
CalloutComponent, CalloutComponent,
DashboardComponent, DashboardComponent,
EnvironmentComponent, EnvironmentComponent,
FallbackSrcDirective, FallbackSrcDirective,
I18nPipe, I18nPipe,
IconComponent, IconComponent,
MoreComponent, MoreComponent,
SearchCiphersPipe, SearchCiphersPipe,
SettingsComponent, SettingsComponent,
StopClickDirective, StopClickDirective,
StopPropDirective, StopPropDirective,
TabsComponent, TabsComponent,
], ],
providers: [], providers: [],
bootstrap: [AppComponent], bootstrap: [AppComponent],
}) })
export class AppModule { } export class AppModule {}

View File

@@ -1,15 +1,15 @@
import { enableProdMode } from '@angular/core'; import { enableProdMode } from "@angular/core";
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
import { isDev } from 'jslib-electron/utils'; import { isDev } from "jslib-electron/utils";
// tslint:disable-next-line // tslint:disable-next-line
require('../scss/styles.scss'); require("../scss/styles.scss");
import { AppModule } from './app.module'; import { AppModule } from "./app.module";
if (!isDev()) { if (!isDev()) {
enableProdMode(); enableProdMode();
} }
platformBrowserDynamic().bootstrapModule(AppModule, { preserveWhitespaces: true }); platformBrowserDynamic().bootstrapModule(AppModule, { preserveWhitespaces: true });

View File

@@ -1,24 +1,24 @@
import { Injectable } from '@angular/core'; import { Injectable } from "@angular/core";
import { import { CanActivate, Router } from "@angular/router";
CanActivate, import { ApiKeyService } from "jslib-common/abstractions/apiKey.service";
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() @Injectable()
export class AuthGuardService implements CanActivate { export class AuthGuardService implements CanActivate {
constructor(private apiKeyService: ApiKeyService, private router: Router, constructor(
private messagingService: MessagingService) { } private apiKeyService: ApiKeyService,
private router: Router,
private messagingService: MessagingService
) {}
async canActivate() { async canActivate() {
const isAuthed = await this.apiKeyService.isAuthenticated(); const isAuthed = await this.apiKeyService.isAuthenticated();
if (!isAuthed) { if (!isAuthed) {
this.messagingService.send('logout'); this.messagingService.send("logout");
return false; return false;
}
return true;
} }
return true;
}
} }

View File

@@ -1,22 +1,19 @@
import { Injectable } from '@angular/core'; import { Injectable } from "@angular/core";
import { import { CanActivate, Router } from "@angular/router";
CanActivate,
Router,
} from '@angular/router';
import { ApiKeyService } from 'jslib-common/abstractions/apiKey.service'; import { ApiKeyService } from "jslib-common/abstractions/apiKey.service";
@Injectable() @Injectable()
export class LaunchGuardService implements CanActivate { export class LaunchGuardService implements CanActivate {
constructor(private apiKeyService: ApiKeyService, private router: Router) { } constructor(private apiKeyService: ApiKeyService, private router: Router) {}
async canActivate() { async canActivate() {
const isAuthed = await this.apiKeyService.isAuthenticated(); const isAuthed = await this.apiKeyService.isAuthenticated();
if (!isAuthed) { if (!isAuthed) {
return true; return true;
}
this.router.navigate(['/tabs/dashboard']);
return false;
} }
this.router.navigate(["/tabs/dashboard"]);
return false;
}
} }

View File

@@ -1,219 +1,222 @@
import { import { APP_INITIALIZER, Injector, NgModule } from "@angular/core";
APP_INITIALIZER,
Injector,
NgModule,
} from '@angular/core';
import { ElectronLogService } from 'jslib-electron/services/electronLog.service'; import { ElectronLogService } from "jslib-electron/services/electronLog.service";
import { ElectronPlatformUtilsService } from 'jslib-electron/services/electronPlatformUtils.service'; import { ElectronPlatformUtilsService } from "jslib-electron/services/electronPlatformUtils.service";
import { ElectronRendererMessagingService } from 'jslib-electron/services/electronRendererMessaging.service'; import { ElectronRendererMessagingService } from "jslib-electron/services/electronRendererMessaging.service";
import { ElectronRendererSecureStorageService } from 'jslib-electron/services/electronRendererSecureStorage.service'; import { ElectronRendererSecureStorageService } from "jslib-electron/services/electronRendererSecureStorage.service";
import { ElectronRendererStorageService } from 'jslib-electron/services/electronRendererStorage.service'; import { ElectronRendererStorageService } from "jslib-electron/services/electronRendererStorage.service";
import { AuthGuardService } from './auth-guard.service'; import { AuthGuardService } from "./auth-guard.service";
import { LaunchGuardService } from './launch-guard.service'; import { LaunchGuardService } from "./launch-guard.service";
import { ConfigurationService } from '../../services/configuration.service'; import { ConfigurationService } from "../../services/configuration.service";
import { I18nService } from '../../services/i18n.service'; import { I18nService } from "../../services/i18n.service";
import { SyncService } from '../../services/sync.service'; import { SyncService } from "../../services/sync.service";
import { BroadcasterService } from 'jslib-angular/services/broadcaster.service'; import { BroadcasterService } from "jslib-angular/services/broadcaster.service";
import { JslibServicesModule } from 'jslib-angular/services/jslib-services.module'; import { JslibServicesModule } from "jslib-angular/services/jslib-services.module";
import { ModalService } from 'jslib-angular/services/modal.service'; import { ModalService } from "jslib-angular/services/modal.service";
import { ValidationService } from 'jslib-angular/services/validation.service'; import { ValidationService } from "jslib-angular/services/validation.service";
import { ApiKeyService } from 'jslib-common/services/apiKey.service'; import { ApiKeyService } from "jslib-common/services/apiKey.service";
import { ConstantsService } from 'jslib-common/services/constants.service'; import { ConstantsService } from "jslib-common/services/constants.service";
import { ContainerService } from 'jslib-common/services/container.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 { ApiService as ApiServiceAbstraction } from "jslib-common/abstractions/api.service";
import { ApiKeyService as ApiKeyServiceAbstraction } from 'jslib-common/abstractions/apiKey.service'; import { ApiKeyService as ApiKeyServiceAbstraction } from "jslib-common/abstractions/apiKey.service";
import { AppIdService as AppIdServiceAbstraction } from 'jslib-common/abstractions/appId.service'; import { AppIdService as AppIdServiceAbstraction } from "jslib-common/abstractions/appId.service";
import { AuthService as AuthServiceAbstraction } from 'jslib-common/abstractions/auth.service'; import { AuthService as AuthServiceAbstraction } from "jslib-common/abstractions/auth.service";
import { BroadcasterService as BroadcasterServiceAbstraction } from 'jslib-common/abstractions/broadcaster.service'; import { BroadcasterService as BroadcasterServiceAbstraction } from "jslib-common/abstractions/broadcaster.service";
import { CryptoService as CryptoServiceAbstraction } from 'jslib-common/abstractions/crypto.service'; import { CryptoService as CryptoServiceAbstraction } from "jslib-common/abstractions/crypto.service";
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from 'jslib-common/abstractions/cryptoFunction.service'; import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "jslib-common/abstractions/cryptoFunction.service";
import { EnvironmentService as EnvironmentServiceAbstraction } from 'jslib-common/abstractions/environment.service'; import { EnvironmentService as EnvironmentServiceAbstraction } from "jslib-common/abstractions/environment.service";
import { I18nService as I18nServiceAbstraction } from 'jslib-common/abstractions/i18n.service'; import { I18nService as I18nServiceAbstraction } from "jslib-common/abstractions/i18n.service";
import { KeyConnectorService as KeyConnectorServiceAbstraction } from 'jslib-common/abstractions/keyConnector.service'; import { KeyConnectorService as KeyConnectorServiceAbstraction } from "jslib-common/abstractions/keyConnector.service";
import { LogService as LogServiceAbstraction } from 'jslib-common/abstractions/log.service'; import { LogService as LogServiceAbstraction } from "jslib-common/abstractions/log.service";
import { MessagingService as MessagingServiceAbstraction } from 'jslib-common/abstractions/messaging.service'; import { MessagingService as MessagingServiceAbstraction } from "jslib-common/abstractions/messaging.service";
import { import { PasswordGenerationService as PasswordGenerationServiceAbstraction } from "jslib-common/abstractions/passwordGeneration.service";
PasswordGenerationService as PasswordGenerationServiceAbstraction, import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "jslib-common/abstractions/platformUtils.service";
} from 'jslib-common/abstractions/passwordGeneration.service'; import { PolicyService as PolicyServiceAbstraction } from "jslib-common/abstractions/policy.service";
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from 'jslib-common/abstractions/platformUtils.service'; import { StateService as StateServiceAbstraction } from "jslib-common/abstractions/state.service";
import { PolicyService as PolicyServiceAbstraction } from 'jslib-common/abstractions/policy.service'; import { StorageService as StorageServiceAbstraction } from "jslib-common/abstractions/storage.service";
import { StateService as StateServiceAbstraction } from 'jslib-common/abstractions/state.service'; import { TokenService as TokenServiceAbstraction } from "jslib-common/abstractions/token.service";
import { StorageService as StorageServiceAbstraction } from 'jslib-common/abstractions/storage.service'; import { UserService as UserServiceAbstraction } from "jslib-common/abstractions/user.service";
import { TokenService as TokenServiceAbstraction } from 'jslib-common/abstractions/token.service'; import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "jslib-common/abstractions/vaultTimeout.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 { ApiService, refreshToken } from "../../services/api.service";
import { AuthService } from '../../services/auth.service'; import { AuthService } from "../../services/auth.service";
function refreshTokenCallback(injector: Injector) { function refreshTokenCallback(injector: Injector) {
return () => { return () => {
const apiKeyService = injector.get(ApiKeyServiceAbstraction); const apiKeyService = injector.get(ApiKeyServiceAbstraction);
const authService = injector.get(AuthServiceAbstraction); const authService = injector.get(AuthServiceAbstraction);
return refreshToken(apiKeyService, authService); return refreshToken(apiKeyService, authService);
}; };
} }
export function initFactory(environmentService: EnvironmentServiceAbstraction, export function initFactory(
i18nService: I18nService, authService: AuthService, platformUtilsService: PlatformUtilsServiceAbstraction, environmentService: EnvironmentServiceAbstraction,
storageService: StorageServiceAbstraction, userService: UserServiceAbstraction, apiService: ApiServiceAbstraction, i18nService: I18nService,
stateService: StateServiceAbstraction, cryptoService: CryptoServiceAbstraction): Function { authService: AuthService,
return async () => { platformUtilsService: PlatformUtilsServiceAbstraction,
await environmentService.setUrlsFromStorage(); storageService: StorageServiceAbstraction,
await i18nService.init(); userService: UserServiceAbstraction,
authService.init(); apiService: ApiServiceAbstraction,
const htmlEl = window.document.documentElement; stateService: StateServiceAbstraction,
htmlEl.classList.add('os_' + platformUtilsService.getDeviceString()); cryptoService: CryptoServiceAbstraction
htmlEl.classList.add('locale_' + i18nService.translationLocale); ): Function {
window.document.title = i18nService.t('bitwardenDirectoryConnector'); 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; let installAction = null;
const installedVersion = await storageService.get<string>(ConstantsService.installedVersionKey); const installedVersion = await storageService.get<string>(ConstantsService.installedVersionKey);
const currentVersion = await platformUtilsService.getApplicationVersion(); const currentVersion = await platformUtilsService.getApplicationVersion();
if (installedVersion == null) { if (installedVersion == null) {
installAction = 'install'; installAction = "install";
} else if (installedVersion !== currentVersion) { } else if (installedVersion !== currentVersion) {
installAction = 'update'; installAction = "update";
} }
if (installAction != null) { if (installAction != null) {
await storageService.save(ConstantsService.installedVersionKey, currentVersion); await storageService.save(ConstantsService.installedVersionKey, currentVersion);
} }
window.setTimeout(async () => { window.setTimeout(async () => {
if (await userService.isAuthenticated()) { if (await userService.isAuthenticated()) {
const profile = await apiService.getProfile(); const profile = await apiService.getProfile();
stateService.save('profileOrganizations', profile.organizations); stateService.save("profileOrganizations", profile.organizations);
} }
}, 500); }, 500);
const containerService = new ContainerService(cryptoService); const containerService = new ContainerService(cryptoService);
containerService.attachToWindow(window); containerService.attachToWindow(window);
}; };
} }
@NgModule({ @NgModule({
imports: [ imports: [JslibServicesModule],
JslibServicesModule, declarations: [],
], providers: [
declarations: [], {
providers: [ provide: APP_INITIALIZER,
{ useFactory: initFactory,
provide: APP_INITIALIZER, deps: [
useFactory: initFactory, EnvironmentServiceAbstraction,
deps: [ I18nServiceAbstraction,
EnvironmentServiceAbstraction, AuthServiceAbstraction,
I18nServiceAbstraction, PlatformUtilsServiceAbstraction,
AuthServiceAbstraction, StorageServiceAbstraction,
PlatformUtilsServiceAbstraction, UserServiceAbstraction,
StorageServiceAbstraction, ApiServiceAbstraction,
UserServiceAbstraction, StateServiceAbstraction,
ApiServiceAbstraction, CryptoServiceAbstraction,
StateServiceAbstraction, ],
CryptoServiceAbstraction, multi: true,
], },
multi: true, { provide: LogServiceAbstraction, useClass: ElectronLogService, deps: [] },
}, {
{ provide: LogServiceAbstraction, useClass: ElectronLogService, deps: [] }, provide: I18nServiceAbstraction,
{ useFactory: (window: Window) => new I18nService(window.navigator.language, "./locales"),
provide: I18nServiceAbstraction, deps: ["WINDOW"],
useFactory: (window: Window) => new I18nService(window.navigator.language, './locales'), },
deps: [ 'WINDOW' ], {
}, provide: MessagingServiceAbstraction,
{ useClass: ElectronRendererMessagingService,
provide: MessagingServiceAbstraction, deps: [BroadcasterServiceAbstraction],
useClass: ElectronRendererMessagingService, },
deps: [ BroadcasterServiceAbstraction ], { provide: StorageServiceAbstraction, useClass: ElectronRendererStorageService },
}, { provide: "SECURE_STORAGE", useClass: ElectronRendererSecureStorageService },
{ provide: StorageServiceAbstraction, useClass: ElectronRendererStorageService }, {
{ provide: 'SECURE_STORAGE', useClass: ElectronRendererSecureStorageService }, provide: PlatformUtilsServiceAbstraction,
{ useFactory: (
provide: PlatformUtilsServiceAbstraction, i18nService: I18nServiceAbstraction,
useFactory: (i18nService: I18nServiceAbstraction, messagingService: MessagingServiceAbstraction, messagingService: MessagingServiceAbstraction,
storageService: StorageServiceAbstraction) => new ElectronPlatformUtilsService(i18nService, storageService: StorageServiceAbstraction
messagingService, true, storageService), ) => new ElectronPlatformUtilsService(i18nService, messagingService, true, storageService),
deps: [ deps: [I18nServiceAbstraction, MessagingServiceAbstraction, StorageServiceAbstraction],
I18nServiceAbstraction, },
MessagingServiceAbstraction, {
StorageServiceAbstraction, provide: CryptoFunctionServiceAbstraction,
], useClass: NodeCryptoFunctionService,
}, deps: [],
{ provide: CryptoFunctionServiceAbstraction, useClass: NodeCryptoFunctionService, deps: [] }, },
{ {
provide: ApiServiceAbstraction, provide: ApiServiceAbstraction,
useFactory: (tokenService: TokenServiceAbstraction, platformUtilsService: PlatformUtilsServiceAbstraction, useFactory: (
environmentService: EnvironmentServiceAbstraction, messagingService: MessagingServiceAbstraction, tokenService: TokenServiceAbstraction,
injector: Injector) => platformUtilsService: PlatformUtilsServiceAbstraction,
new ApiService(tokenService, platformUtilsService, environmentService, refreshTokenCallback(injector), environmentService: EnvironmentServiceAbstraction,
async (expired: boolean) => messagingService.send('logout', { expired: expired })), messagingService: MessagingServiceAbstraction,
deps: [ injector: Injector
TokenServiceAbstraction, ) =>
PlatformUtilsServiceAbstraction, new ApiService(
EnvironmentServiceAbstraction, tokenService,
MessagingServiceAbstraction, platformUtilsService,
Injector, environmentService,
], refreshTokenCallback(injector),
}, async (expired: boolean) => messagingService.send("logout", { expired: expired })
{ ),
provide: ApiKeyServiceAbstraction, deps: [
useClass: ApiKeyService, TokenServiceAbstraction,
deps: [ PlatformUtilsServiceAbstraction,
TokenServiceAbstraction, EnvironmentServiceAbstraction,
StorageServiceAbstraction, MessagingServiceAbstraction,
], Injector,
}, ],
{ },
provide: AuthServiceAbstraction, {
useClass: AuthService, provide: ApiKeyServiceAbstraction,
deps: [ useClass: ApiKeyService,
CryptoServiceAbstraction, deps: [TokenServiceAbstraction, StorageServiceAbstraction],
ApiServiceAbstraction, },
UserServiceAbstraction, {
TokenServiceAbstraction, provide: AuthServiceAbstraction,
AppIdServiceAbstraction, useClass: AuthService,
I18nServiceAbstraction, deps: [
PlatformUtilsServiceAbstraction, CryptoServiceAbstraction,
MessagingServiceAbstraction, ApiServiceAbstraction,
VaultTimeoutServiceAbstraction, UserServiceAbstraction,
LogServiceAbstraction, TokenServiceAbstraction,
ApiKeyServiceAbstraction, AppIdServiceAbstraction,
CryptoFunctionServiceAbstraction, I18nServiceAbstraction,
EnvironmentServiceAbstraction, PlatformUtilsServiceAbstraction,
KeyConnectorServiceAbstraction, MessagingServiceAbstraction,
], VaultTimeoutServiceAbstraction,
}, LogServiceAbstraction,
{ ApiKeyServiceAbstraction,
provide: ConfigurationService, CryptoFunctionServiceAbstraction,
useClass: ConfigurationService, EnvironmentServiceAbstraction,
deps: [ KeyConnectorServiceAbstraction,
StorageServiceAbstraction, ],
'SECURE_STORAGE', },
], {
}, provide: ConfigurationService,
{ useClass: ConfigurationService,
provide: SyncService, deps: [StorageServiceAbstraction, "SECURE_STORAGE"],
useClass: SyncService, },
deps: [ {
ConfigurationService, provide: SyncService,
LogServiceAbstraction, useClass: SyncService,
CryptoFunctionServiceAbstraction, deps: [
ApiServiceAbstraction, ConfigurationService,
MessagingServiceAbstraction, LogServiceAbstraction,
I18nServiceAbstraction, CryptoFunctionServiceAbstraction,
EnvironmentServiceAbstraction, ApiServiceAbstraction,
], MessagingServiceAbstraction,
}, I18nServiceAbstraction,
AuthGuardService, EnvironmentServiceAbstraction,
LaunchGuardService, ],
], },
AuthGuardService,
LaunchGuardService,
],
}) })
export class ServicesModule { export class ServicesModule {}
}

View File

@@ -1,99 +1,110 @@
<div class="card mb-3"> <div class="card mb-3">
<h3 class="card-header">{{'sync' | i18n}}</h3> <h3 class="card-header">{{ "sync" | i18n }}</h3>
<div class="card-body"> <div class="card-body">
<p> <p>
{{'lastGroupSync' | i18n}}: {{ "lastGroupSync" | i18n }}:
<span *ngIf="!lastGroupSync">-</span> <span *ngIf="!lastGroupSync">-</span>
{{lastGroupSync | date:'medium'}} {{ lastGroupSync | date: "medium" }}
<br /> {{'lastUserSync' | i18n}}: <br />
<span *ngIf="!lastUserSync">-</span> {{ "lastUserSync" | i18n }}:
{{lastUserSync | date:'medium'}} <span *ngIf="!lastUserSync">-</span>
</p> {{ lastUserSync | date: "medium" }}
<p> </p>
{{'syncStatus' | i18n}}: <p>
<strong *ngIf="syncRunning" class="text-success">{{'running' | i18n}}</strong> {{ "syncStatus" | i18n }}:
<strong *ngIf="!syncRunning" class="text-danger">{{'stopped' | i18n}}</strong> <strong *ngIf="syncRunning" class="text-success">{{ "running" | i18n }}</strong>
</p> <strong *ngIf="!syncRunning" class="text-danger">{{ "stopped" | i18n }}</strong>
<form #startForm [appApiAction]="startPromise" class="d-inline"> </p>
<button (click)="start()" class="btn btn-primary" <form #startForm [appApiAction]="startPromise" class="d-inline">
[disabled]="startForm.loading"> <button (click)="start()" class="btn btn-primary" [disabled]="startForm.loading">
<i class="fa fa-play fa-fw" [hidden]="startForm.loading"></i> <i class="fa fa-play fa-fw" [hidden]="startForm.loading"></i>
<i class="fa fa-spinner fa-fw fa-spin" [hidden]="!startForm.loading"></i> <i class="fa fa-spinner fa-fw fa-spin" [hidden]="!startForm.loading"></i>
{{'startSync' | i18n}} {{ "startSync" | i18n }}
</button> </button>
</form> </form>
<button (click)="stop()" class="btn btn-primary"> <button (click)="stop()" class="btn btn-primary">
<i class="fa fa-stop fa-fw"></i> <i class="fa fa-stop fa-fw"></i>
{{'stopSync' | i18n}} {{ "stopSync" | i18n }}
</button> </button>
<form #syncForm [appApiAction]="syncPromise" class="d-inline"> <form #syncForm [appApiAction]="syncPromise" class="d-inline">
<button (click)="sync()" class="btn btn-primary" <button (click)="sync()" class="btn btn-primary" [disabled]="syncForm.loading">
[disabled]="syncForm.loading"> <i class="fa fa-refresh fa-fw" [ngClass]="{ 'fa-spin': syncForm.loading }"></i>
<i class="fa fa-refresh fa-fw" [ngClass]="{'fa-spin': syncForm.loading}"></i> {{ "syncNow" | i18n }}
{{'syncNow' | i18n}} </button>
</button> </form>
</form> </div>
</div>
</div> </div>
<div class="card"> <div class="card">
<h3 class="card-header">{{'testing' | i18n}}</h3> <h3 class="card-header">{{ "testing" | i18n }}</h3>
<div class="card-body"> <div class="card-body">
<p>{{'testingDesc' | i18n}}</p> <p>{{ "testingDesc" | i18n }}</p>
<form #simForm [appApiAction]="simPromise" class="d-inline"> <form #simForm [appApiAction]="simPromise" class="d-inline">
<button (click)="simulate()" class="btn btn-primary" <button (click)="simulate()" class="btn btn-primary" [disabled]="simForm.loading">
[disabled]="simForm.loading"> <i class="fa fa-spinner fa-fw fa-spin" [hidden]="!simForm.loading"></i>
<i class="fa fa-spinner fa-fw fa-spin" [hidden]="!simForm.loading"></i> <i class="fa fa-bug fa-fw" [hidden]="simForm.loading"></i>
<i class="fa fa-bug fa-fw" [hidden]="simForm.loading"></i> {{ "testNow" | i18n }}
{{'testNow' | i18n}} </button>
</button> </form>
</form> <div class="form-check mt-2">
<div class="form-check mt-2"> <input
<input class="form-check-input" type="checkbox" id="simSinceLast" [(ngModel)]="simSinceLast"> class="form-check-input"
<label class="form-check-label" for="simSinceLast">{{'testLastSync' | i18n}}</label> type="checkbox"
</div> id="simSinceLast"
<ng-container *ngIf="!simForm.loading && (simUsers || simGroups)"> [(ngModel)]="simSinceLast"
<hr /> />
<div class="row"> <label class="form-check-label" for="simSinceLast">{{ "testLastSync" | i18n }}</label>
<div class="col-lg">
<h4>{{'users' | i18n}}</h4>
<ul class="fa-ul testing-list" *ngIf="simEnabledUsers && simEnabledUsers.length">
<li *ngFor="let u of simEnabledUsers" title="{{u.referenceId}}">
<i class="fa-li fa fa-user"></i>
{{u.displayName}}
</li>
</ul>
<p *ngIf="!simEnabledUsers || !simEnabledUsers.length">{{'noUsers' | i18n}}</p>
<h4>{{'disabledUsers' | i18n}}</h4>
<ul class="fa-ul testing-list" *ngIf="simDisabledUsers && simDisabledUsers.length">
<li *ngFor="let u of simDisabledUsers" title="{{u.referenceId}}">
<i class="fa-li fa fa-user"></i>
{{u.displayName}}
</li>
</ul>
<p *ngIf="!simDisabledUsers || !simDisabledUsers.length">{{'noUsers' | i18n}}</p>
<h4>{{'deletedUsers' | i18n}}</h4>
<ul class="fa-ul testing-list" *ngIf="simDeletedUsers && simDeletedUsers.length">
<li *ngFor="let u of simDeletedUsers" title="{{u.referenceId}}">
<i class="fa-li fa fa-user"></i>
{{u.displayName}}
</li>
</ul>
<p *ngIf="!simDeletedUsers || !simDeletedUsers.length">{{'noUsers' | i18n}}</p>
</div>
<div class="col-lg">
<h4>{{'groups' | i18n}}</h4>
<ul class="fa-ul testing-list" *ngIf="simGroups && simGroups.length">
<li *ngFor="let g of simGroups" title="{{g.referenceId}}">
<i class="fa-li fa fa-sitemap"></i>
{{g.displayName}}
<ul class="small" *ngIf="g.users && g.users.length">
<li *ngFor="let u of g.users" title="{{u.referenceId}}">{{u.displayName}}</li>
</ul>
</li>
</ul>
<p *ngIf="!simGroups || !simGroups.length">{{'noGroups' | i18n}}</p>
</div>
</div>
</ng-container>
</div> </div>
<ng-container *ngIf="!simForm.loading && (simUsers || simGroups)">
<hr />
<div class="row">
<div class="col-lg">
<h4>{{ "users" | i18n }}</h4>
<ul class="fa-ul testing-list" *ngIf="simEnabledUsers && simEnabledUsers.length">
<li *ngFor="let u of simEnabledUsers" title="{{ u.referenceId }}">
<i class="fa-li fa fa-user"></i>
{{ u.displayName }}
</li>
</ul>
<p *ngIf="!simEnabledUsers || !simEnabledUsers.length">
{{ "noUsers" | i18n }}
</p>
<h4>{{ "disabledUsers" | i18n }}</h4>
<ul class="fa-ul testing-list" *ngIf="simDisabledUsers && simDisabledUsers.length">
<li *ngFor="let u of simDisabledUsers" title="{{ u.referenceId }}">
<i class="fa-li fa fa-user"></i>
{{ u.displayName }}
</li>
</ul>
<p *ngIf="!simDisabledUsers || !simDisabledUsers.length">
{{ "noUsers" | i18n }}
</p>
<h4>{{ "deletedUsers" | i18n }}</h4>
<ul class="fa-ul testing-list" *ngIf="simDeletedUsers && simDeletedUsers.length">
<li *ngFor="let u of simDeletedUsers" title="{{ u.referenceId }}">
<i class="fa-li fa fa-user"></i>
{{ u.displayName }}
</li>
</ul>
<p *ngIf="!simDeletedUsers || !simDeletedUsers.length">
{{ "noUsers" | i18n }}
</p>
</div>
<div class="col-lg">
<h4>{{ "groups" | i18n }}</h4>
<ul class="fa-ul testing-list" *ngIf="simGroups && simGroups.length">
<li *ngFor="let g of simGroups" title="{{ g.referenceId }}">
<i class="fa-li fa fa-sitemap"></i>
{{ g.displayName }}
<ul class="small" *ngIf="g.users && g.users.length">
<li *ngFor="let u of g.users" title="{{ u.referenceId }}">
{{ u.displayName }}
</li>
</ul>
</li>
</ul>
<p *ngIf="!simGroups || !simGroups.length">{{ "noGroups" | i18n }}</p>
</div>
</div>
</ng-container>
</div>
</div> </div>

View File

@@ -1,121 +1,128 @@
import { import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core";
ChangeDetectorRef,
Component,
NgZone,
OnDestroy,
OnInit,
} from '@angular/core';
import { BroadcasterService } from 'jslib-common/abstractions/broadcaster.service'; import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { MessagingService } from 'jslib-common/abstractions/messaging.service'; import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { StateService } from 'jslib-common/abstractions/state.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 { GroupEntry } from "../../models/groupEntry";
import { SimResult } from '../../models/simResult'; import { SimResult } from "../../models/simResult";
import { UserEntry } from '../../models/userEntry'; import { UserEntry } from "../../models/userEntry";
import { ConfigurationService } from '../../services/configuration.service'; import { ConfigurationService } from "../../services/configuration.service";
import { ConnectorUtils } from '../../utils'; import { ConnectorUtils } from "../../utils";
const BroadcasterSubscriptionId = 'DashboardComponent'; const BroadcasterSubscriptionId = "DashboardComponent";
@Component({ @Component({
selector: 'app-dashboard', selector: "app-dashboard",
templateUrl: 'dashboard.component.html', templateUrl: "dashboard.component.html",
}) })
export class DashboardComponent implements OnInit, OnDestroy { export class DashboardComponent implements OnInit, OnDestroy {
simGroups: GroupEntry[]; simGroups: GroupEntry[];
simUsers: UserEntry[]; simUsers: UserEntry[];
simEnabledUsers: UserEntry[] = []; simEnabledUsers: UserEntry[] = [];
simDisabledUsers: UserEntry[] = []; simDisabledUsers: UserEntry[] = [];
simDeletedUsers: UserEntry[] = []; simDeletedUsers: UserEntry[] = [];
simPromise: Promise<SimResult>; simPromise: Promise<SimResult>;
simSinceLast: boolean = false; simSinceLast: boolean = false;
syncPromise: Promise<[GroupEntry[], UserEntry[]]>; syncPromise: Promise<[GroupEntry[], UserEntry[]]>;
startPromise: Promise<any>; startPromise: Promise<any>;
lastGroupSync: Date; lastGroupSync: Date;
lastUserSync: Date; lastUserSync: Date;
syncRunning: boolean; syncRunning: boolean;
constructor(private i18nService: I18nService, private syncService: SyncService, constructor(
private configurationService: ConfigurationService, private broadcasterService: BroadcasterService, private i18nService: I18nService,
private ngZone: NgZone, private messagingService: MessagingService, private syncService: SyncService,
private platformUtilsService: PlatformUtilsService, private changeDetectorRef: ChangeDetectorRef, private configurationService: ConfigurationService,
private stateService: StateService) { } private broadcasterService: BroadcasterService,
private ngZone: NgZone,
private messagingService: MessagingService,
private platformUtilsService: PlatformUtilsService,
private changeDetectorRef: ChangeDetectorRef,
private stateService: StateService
) {}
async ngOnInit() { async ngOnInit() {
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => { this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
this.ngZone.run(async () => { this.ngZone.run(async () => {
switch (message.command) { switch (message.command) {
case 'dirSyncCompleted': case "dirSyncCompleted":
this.updateLastSync(); this.updateLastSync();
break; break;
default: default:
break; 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;
} }
}
private async updateLastSync() { this.changeDetectorRef.detectChanges();
this.lastGroupSync = await this.configurationService.getLastGroupSyncDate(); });
this.lastUserSync = await this.configurationService.getLastUserSyncDate(); });
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();
}
} }

View File

@@ -1,32 +1,38 @@
<div class="row"> <div class="row">
<div class="col-sm"> <div class="col-sm">
<div class="card"> <div class="card">
<h3 class="card-header">{{'about' | i18n}}</h3> <h3 class="card-header">{{ "about" | i18n }}</h3>
<div class="card-body"> <div class="card-body">
<p> <p>
{{'bitwardenDirectoryConnector' | i18n}} {{ "bitwardenDirectoryConnector" | i18n }}
<br /> {{'version' | i18n : version}} <br />
<br /> &copy; Bitwarden Inc. LLC 2015-{{year}} {{ "version" | i18n: version }} <br />
</p> &copy; Bitwarden Inc. LLC 2015-{{ year }}
<button class="btn btn-primary" type="button" (click)="update()" [disabled]="checkingForUpdate"> </p>
<i class="fa fa-download fa-fw" [hidden]="checkingForUpdate"></i> <button
<i class="fa fa-spinner fa-fw fa-spin" [hidden]="!checkingForUpdate"></i> class="btn btn-primary"
{{'checkForUpdates' | i18n}} type="button"
</button> (click)="update()"
</div> [disabled]="checkingForUpdate"
</div> >
<i class="fa fa-download fa-fw" [hidden]="checkingForUpdate"></i>
<i class="fa fa-spinner fa-fw fa-spin" [hidden]="!checkingForUpdate"></i>
{{ "checkForUpdates" | i18n }}
</button>
</div>
</div> </div>
<div class="col-sm"> </div>
<div class="card"> <div class="col-sm">
<h3 class="card-header">{{'other' | i18n}}</h3> <div class="card">
<div class="card-body"> <h3 class="card-header">{{ "other" | i18n }}</h3>
<button class="btn btn-primary" type="button" (click)="logOut()"> <div class="card-body">
{{'logOut' | i18n}} <button class="btn btn-primary" type="button" (click)="logOut()">
</button> {{ "logOut" | i18n }}
<button class="btn btn-primary" type="button" (click)="clearCache()"> </button>
{{'clearSyncCache' | i18n}} <button class="btn btn-primary" type="button" (click)="clearCache()">
</button> {{ "clearSyncCache" | i18n }}
</div> </button>
</div> </div>
</div> </div>
</div>
</div> </div>

View File

@@ -1,75 +1,77 @@
import { import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core";
ChangeDetectorRef,
Component,
NgZone,
OnDestroy,
OnInit,
} from '@angular/core';
import { BroadcasterService } from 'jslib-common/abstractions/broadcaster.service'; import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { MessagingService } from 'jslib-common/abstractions/messaging.service'; import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.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({ @Component({
selector: 'app-more', selector: "app-more",
templateUrl: 'more.component.html', templateUrl: "more.component.html",
}) })
export class MoreComponent implements OnInit { export class MoreComponent implements OnInit {
version: string; version: string;
year: string; year: string;
checkingForUpdate = false; checkingForUpdate = false;
constructor(private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, constructor(
private messagingService: MessagingService, private configurationService: ConfigurationService, private platformUtilsService: PlatformUtilsService,
private broadcasterService: BroadcasterService, private i18nService: I18nService,
private ngZone: NgZone, private changeDetectorRef: ChangeDetectorRef) { } private messagingService: MessagingService,
private configurationService: ConfigurationService,
private broadcasterService: BroadcasterService,
private ngZone: NgZone,
private changeDetectorRef: ChangeDetectorRef
) {}
async ngOnInit() { async ngOnInit() {
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => { this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
this.ngZone.run(async () => { this.ngZone.run(async () => {
switch (message.command) { switch (message.command) {
case 'checkingForUpdate': case "checkingForUpdate":
this.checkingForUpdate = true; this.checkingForUpdate = true;
break; break;
case 'doneCheckingForUpdate': case "doneCheckingForUpdate":
this.checkingForUpdate = false; this.checkingForUpdate = false;
break; break;
default: default:
break; 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 clearCache() { this.changeDetectorRef.detectChanges();
await this.configurationService.clearStatefulSettings(true); });
this.platformUtilsService.showToast('success', null, this.i18nService.t('syncCacheCleared')); });
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"));
}
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,156 +1,162 @@
import { import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core";
ChangeDetectorRef,
Component,
NgZone,
OnDestroy,
OnInit,
} from '@angular/core';
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from 'jslib-common/abstractions/log.service'; import { LogService } from "jslib-common/abstractions/log.service";
import { StateService } from 'jslib-common/abstractions/state.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 { AzureConfiguration } from "../../models/azureConfiguration";
import { GSuiteConfiguration } from '../../models/gsuiteConfiguration'; import { GSuiteConfiguration } from "../../models/gsuiteConfiguration";
import { LdapConfiguration } from '../../models/ldapConfiguration'; import { LdapConfiguration } from "../../models/ldapConfiguration";
import { OktaConfiguration } from '../../models/oktaConfiguration'; import { OktaConfiguration } from "../../models/oktaConfiguration";
import { OneLoginConfiguration } from '../../models/oneLoginConfiguration'; import { OneLoginConfiguration } from "../../models/oneLoginConfiguration";
import { SyncConfiguration } from '../../models/syncConfiguration'; import { SyncConfiguration } from "../../models/syncConfiguration";
import { ConnectorUtils } from '../../utils'; import { ConnectorUtils } from "../../utils";
@Component({ @Component({
selector: 'app-settings', selector: "app-settings",
templateUrl: 'settings.component.html', templateUrl: "settings.component.html",
}) })
export class SettingsComponent implements OnInit, OnDestroy { export class SettingsComponent implements OnInit, OnDestroy {
directory: DirectoryType; directory: DirectoryType;
directoryType = DirectoryType; directoryType = DirectoryType;
ldap = new LdapConfiguration(); ldap = new LdapConfiguration();
gsuite = new GSuiteConfiguration(); gsuite = new GSuiteConfiguration();
azure = new AzureConfiguration(); azure = new AzureConfiguration();
okta = new OktaConfiguration(); okta = new OktaConfiguration();
oneLogin = new OneLoginConfiguration(); oneLogin = new OneLoginConfiguration();
sync = new SyncConfiguration(); sync = new SyncConfiguration();
directoryOptions: any[]; directoryOptions: any[];
showLdapPassword: boolean = false; showLdapPassword: boolean = false;
showAzureKey: boolean = false; showAzureKey: boolean = false;
showOktaKey: boolean = false; showOktaKey: boolean = false;
showOneLoginSecret: boolean = false; showOneLoginSecret: boolean = false;
constructor(private i18nService: I18nService, private configurationService: ConfigurationService, constructor(
private changeDetectorRef: ChangeDetectorRef, private ngZone: NgZone, private i18nService: I18nService,
private stateService: StateService, private logService: LogService) { private configurationService: ConfigurationService,
this.directoryOptions = [ private changeDetectorRef: ChangeDetectorRef,
{ name: i18nService.t('select'), value: null }, private ngZone: NgZone,
{ name: 'Active Directory / LDAP', value: DirectoryType.Ldap }, private stateService: StateService,
{ name: 'Azure Active Directory', value: DirectoryType.AzureActiveDirectory }, private logService: LogService
{ name: 'G Suite (Google)', value: DirectoryType.GSuite }, ) {
{ name: 'Okta', value: DirectoryType.Okta }, this.directoryOptions = [
{ name: 'OneLogin', value: DirectoryType.OneLogin }, { 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<LdapConfiguration>(DirectoryType.Ldap)) ||
this.ldap;
this.gsuite =
(await this.configurationService.getDirectory<GSuiteConfiguration>(DirectoryType.GSuite)) ||
this.gsuite;
this.azure =
(await this.configurationService.getDirectory<AzureConfiguration>(
DirectoryType.AzureActiveDirectory
)) || this.azure;
this.okta =
(await this.configurationService.getDirectory<OktaConfiguration>(DirectoryType.Okta)) ||
this.okta;
this.oneLogin =
(await this.configurationService.getDirectory<OneLoginConfiguration>(
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() { const reader = new FileReader();
this.directory = await this.configurationService.getDirectoryType(); reader.readAsText(filePicker.files[0], "utf-8");
this.ldap = (await this.configurationService.getDirectory<LdapConfiguration>(DirectoryType.Ldap)) || reader.onload = (evt) => {
this.ldap; this.ngZone.run(async () => {
this.gsuite = (await this.configurationService.getDirectory<GSuiteConfiguration>(DirectoryType.GSuite)) || try {
this.gsuite; const result = JSON.parse((evt.target as FileReader).result as string);
this.azure = (await this.configurationService.getDirectory<AzureConfiguration>( if (result.client_email != null && result.private_key != null) {
DirectoryType.AzureActiveDirectory)) || this.azure; this.gsuite.clientEmail = result.client_email;
this.okta = (await this.configurationService.getDirectory<OktaConfiguration>( this.gsuite.privateKey = result.private_key;
DirectoryType.Okta)) || this.okta; }
this.oneLogin = (await this.configurationService.getDirectory<OneLoginConfiguration>( } catch (e) {
DirectoryType.OneLogin)) || this.oneLogin; this.logService.error(e);
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); this.changeDetectorRef.detectChanges();
await this.configurationService.saveDirectory(DirectoryType.Ldap, this.ldap); });
await this.configurationService.saveDirectory(DirectoryType.GSuite, this.gsuite);
await this.configurationService.saveDirectory(DirectoryType.AzureActiveDirectory, this.azure); // reset file input
await this.configurationService.saveDirectory(DirectoryType.Okta, this.okta); // ref: https://stackoverflow.com/a/20552042
await this.configurationService.saveDirectory(DirectoryType.OneLogin, this.oneLogin); filePicker.type = "";
await this.configurationService.saveSync(this.sync); 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() { (this.ldap as any)[id] = filePicker.files[0].path;
const filePicker = (document.getElementById('keyFile') as HTMLInputElement); // reset file input
if (filePicker.files == null || filePicker.files.length < 0) { // ref: https://stackoverflow.com/a/20552042
return; filePicker.type = "";
} filePicker.type = "file";
filePicker.value = "";
}
const reader = new FileReader(); toggleLdapPassword() {
reader.readAsText(filePicker.files[0], 'utf-8'); this.showLdapPassword = !this.showLdapPassword;
reader.onload = evt => { document.getElementById("password").focus();
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();
});
// reset file input toggleAzureKey() {
// ref: https://stackoverflow.com/a/20552042 this.showAzureKey = !this.showAzureKey;
filePicker.type = ''; document.getElementById("secretKey").focus();
filePicker.type = 'file'; }
filePicker.value = '';
};
}
setSslPath(id: string) { toggleOktaKey() {
const filePicker = (document.getElementById(id + '_file') as HTMLInputElement); this.showOktaKey = !this.showOktaKey;
if (filePicker.files == null || filePicker.files.length < 0) { document.getElementById("oktaToken").focus();
return; }
}
(this.ldap as any)[id] = filePicker.files[0].path; toggleOneLoginSecret() {
// reset file input this.showOneLoginSecret = !this.showOneLoginSecret;
// ref: https://stackoverflow.com/a/20552042 document.getElementById("oneLoginClientSecret").focus();
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();
}
} }

View File

@@ -1,23 +1,23 @@
<div class="container-fluid"> <div class="container-fluid">
<ul class="nav nav-tabs mb-3"> <ul class="nav nav-tabs mb-3">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" routerLink="dashboard" routerLinkActive="active"> <a class="nav-link" routerLink="dashboard" routerLinkActive="active">
<i class="fa fa-dashboard"></i> <i class="fa fa-dashboard"></i>
{{'dashboard' | i18n}} {{ "dashboard" | i18n }}
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" routerLink="settings" routerLinkActive="active"> <a class="nav-link" routerLink="settings" routerLinkActive="active">
<i class="fa fa-cogs"></i> <i class="fa fa-cogs"></i>
{{'settings' | i18n}} {{ "settings" | i18n }}
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" routerLink="more" routerLinkActive="active"> <a class="nav-link" routerLink="more" routerLinkActive="active">
<i class="fa fa-sliders"></i> <i class="fa fa-sliders"></i>
{{'more' | i18n}} {{ "more" | i18n }}
</a> </a>
</li> </li>
</ul> </ul>
<router-outlet></router-outlet> <router-outlet></router-outlet>
</div> </div>

View File

@@ -1,7 +1,7 @@
import { Component } from '@angular/core'; import { Component } from "@angular/core";
@Component({ @Component({
selector: 'app-tabs', selector: "app-tabs",
templateUrl: 'tabs.component.html', templateUrl: "tabs.component.html",
}) })
export class TabsComponent { } export class TabsComponent {}

View File

@@ -1,188 +1,286 @@
import * as fs from 'fs'; import * as fs from "fs";
import * as path from 'path'; 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 { ConfigurationService } from "./services/configuration.service";
import { I18nService } from './services/i18n.service'; import { I18nService } from "./services/i18n.service";
import { KeytarSecureStorageService } from './services/keytarSecureStorage.service'; import { KeytarSecureStorageService } from "./services/keytarSecureStorage.service";
import { LowdbStorageService } from './services/lowdbStorage.service'; import { LowdbStorageService } from "./services/lowdbStorage.service";
import { NodeApiService } from './services/nodeApi.service'; import { NodeApiService } from "./services/nodeApi.service";
import { SyncService } from './services/sync.service'; import { SyncService } from "./services/sync.service";
import { CliPlatformUtilsService } from 'jslib-node/cli/services/cliPlatformUtils.service'; import { CliPlatformUtilsService } from "jslib-node/cli/services/cliPlatformUtils.service";
import { ConsoleLogService } from 'jslib-node/cli/services/consoleLog.service'; import { ConsoleLogService } from "jslib-node/cli/services/consoleLog.service";
import { NodeCryptoFunctionService } from 'jslib-node/services/nodeCryptoFunction.service'; import { NodeCryptoFunctionService } from "jslib-node/services/nodeCryptoFunction.service";
import { ApiKeyService } from 'jslib-common/services/apiKey.service'; import { ApiKeyService } from "jslib-common/services/apiKey.service";
import { AppIdService } from 'jslib-common/services/appId.service'; import { AppIdService } from "jslib-common/services/appId.service";
import { CipherService } from 'jslib-common/services/cipher.service'; import { CipherService } from "jslib-common/services/cipher.service";
import { CollectionService } from 'jslib-common/services/collection.service'; import { CollectionService } from "jslib-common/services/collection.service";
import { ConstantsService } from 'jslib-common/services/constants.service'; import { ConstantsService } from "jslib-common/services/constants.service";
import { ContainerService } from 'jslib-common/services/container.service'; import { ContainerService } from "jslib-common/services/container.service";
import { CryptoService } from 'jslib-common/services/crypto.service'; import { CryptoService } from "jslib-common/services/crypto.service";
import { EnvironmentService } from 'jslib-common/services/environment.service'; import { EnvironmentService } from "jslib-common/services/environment.service";
import { FileUploadService } from 'jslib-common/services/fileUpload.service'; import { FileUploadService } from "jslib-common/services/fileUpload.service";
import { FolderService } from 'jslib-common/services/folder.service'; import { FolderService } from "jslib-common/services/folder.service";
import { KeyConnectorService } from 'jslib-common/services/keyConnector.service'; import { KeyConnectorService } from "jslib-common/services/keyConnector.service";
import { NoopMessagingService } from 'jslib-common/services/noopMessaging.service'; import { NoopMessagingService } from "jslib-common/services/noopMessaging.service";
import { PasswordGenerationService } from 'jslib-common/services/passwordGeneration.service'; import { PasswordGenerationService } from "jslib-common/services/passwordGeneration.service";
import { PolicyService } from 'jslib-common/services/policy.service'; import { PolicyService } from "jslib-common/services/policy.service";
import { SearchService } from 'jslib-common/services/search.service'; import { SearchService } from "jslib-common/services/search.service";
import { SendService } from 'jslib-common/services/send.service'; import { SendService } from "jslib-common/services/send.service";
import { SettingsService } from 'jslib-common/services/settings.service'; import { SettingsService } from "jslib-common/services/settings.service";
import { SyncService as LoginSyncService } from 'jslib-common/services/sync.service'; import { SyncService as LoginSyncService } from "jslib-common/services/sync.service";
import { TokenService } from 'jslib-common/services/token.service'; import { TokenService } from "jslib-common/services/token.service";
import { UserService } from 'jslib-common/services/user.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 { Program } from "./program";
import { refreshToken } from './services/api.service'; import { refreshToken } from "./services/api.service";
// tslint:disable-next-line // tslint:disable-next-line
const packageJson = require('./package.json'); const packageJson = require("./package.json");
export let searchService: SearchService = null; export let searchService: SearchService = null;
export class Main { export class Main {
dataFilePath: string; dataFilePath: string;
logService: ConsoleLogService; logService: ConsoleLogService;
messagingService: NoopMessagingService; messagingService: NoopMessagingService;
storageService: LowdbStorageService; storageService: LowdbStorageService;
secureStorageService: StorageServiceAbstraction; secureStorageService: StorageServiceAbstraction;
i18nService: I18nService; i18nService: I18nService;
platformUtilsService: CliPlatformUtilsService; platformUtilsService: CliPlatformUtilsService;
constantsService: ConstantsService; constantsService: ConstantsService;
cryptoService: CryptoService; cryptoService: CryptoService;
tokenService: TokenService; tokenService: TokenService;
appIdService: AppIdService; appIdService: AppIdService;
apiService: NodeApiService; apiService: NodeApiService;
environmentService: EnvironmentService; environmentService: EnvironmentService;
apiKeyService: ApiKeyService; apiKeyService: ApiKeyService;
userService: UserService; userService: UserService;
containerService: ContainerService; containerService: ContainerService;
cryptoFunctionService: NodeCryptoFunctionService; cryptoFunctionService: NodeCryptoFunctionService;
authService: AuthService; authService: AuthService;
configurationService: ConfigurationService; configurationService: ConfigurationService;
collectionService: CollectionService; collectionService: CollectionService;
cipherService: CipherService; cipherService: CipherService;
fileUploadService: FileUploadService; fileUploadService: FileUploadService;
folderService: FolderService; folderService: FolderService;
searchService: SearchService; searchService: SearchService;
sendService: SendService; sendService: SendService;
settingsService: SettingsService; settingsService: SettingsService;
syncService: SyncService; syncService: SyncService;
passwordGenerationService: PasswordGenerationService; passwordGenerationService: PasswordGenerationService;
policyService: PolicyService; policyService: PolicyService;
loginSyncService: LoginSyncService; loginSyncService: LoginSyncService;
keyConnectorService: KeyConnectorService; keyConnectorService: KeyConnectorService;
program: Program; program: Program;
constructor() { constructor() {
const applicationName = 'Bitwarden Directory Connector'; const applicationName = "Bitwarden Directory Connector";
if (process.env.BITWARDENCLI_CONNECTOR_APPDATA_DIR) { if (process.env.BITWARDENCLI_CONNECTOR_APPDATA_DIR) {
this.dataFilePath = path.resolve(process.env.BITWARDENCLI_CONNECTOR_APPDATA_DIR); this.dataFilePath = path.resolve(process.env.BITWARDENCLI_CONNECTOR_APPDATA_DIR);
} else if (process.env.BITWARDEN_CONNECTOR_APPDATA_DIR) { } else if (process.env.BITWARDEN_CONNECTOR_APPDATA_DIR) {
this.dataFilePath = path.resolve(process.env.BITWARDEN_CONNECTOR_APPDATA_DIR); this.dataFilePath = path.resolve(process.env.BITWARDEN_CONNECTOR_APPDATA_DIR);
} else if (fs.existsSync(path.join(__dirname, 'bitwarden-connector-appdata'))) { } else if (fs.existsSync(path.join(__dirname, "bitwarden-connector-appdata"))) {
this.dataFilePath = path.join(__dirname, 'bitwarden-connector-appdata'); this.dataFilePath = path.join(__dirname, "bitwarden-connector-appdata");
} else if (process.platform === 'darwin') { } else if (process.platform === "darwin") {
this.dataFilePath = path.join(process.env.HOME, 'Library/Application Support/' + applicationName); this.dataFilePath = path.join(
} else if (process.platform === 'win32') { process.env.HOME,
this.dataFilePath = path.join(process.env.APPDATA, applicationName); "Library/Application Support/" + applicationName
} else if (process.env.XDG_CONFIG_HOME) { );
this.dataFilePath = path.join(process.env.XDG_CONFIG_HOME, applicationName); } else if (process.platform === "win32") {
} else { this.dataFilePath = path.join(process.env.APPDATA, applicationName);
this.dataFilePath = path.join(process.env.HOME, '.config/' + applicationName); } else if (process.env.XDG_CONFIG_HOME) {
} this.dataFilePath = path.join(process.env.XDG_CONFIG_HOME, applicationName);
} else {
const plaintextSecrets = process.env.BITWARDENCLI_CONNECTOR_PLAINTEXT_SECRETS === 'true'; this.dataFilePath = path.join(process.env.HOME, ".config/" + applicationName);
this.i18nService = new I18nService('en', './locales');
this.platformUtilsService = new CliPlatformUtilsService('connector', packageJson);
this.logService = new ConsoleLogService(this.platformUtilsService.isDev(),
level => process.env.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);
} }
async run() { const plaintextSecrets = process.env.BITWARDENCLI_CONNECTOR_PLAINTEXT_SECRETS === "true";
await this.init(); this.i18nService = new I18nService("en", "./locales");
this.program.run(); 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() { this.loginSyncService = new LoginSyncService(
await this.tokenService.clearToken(); this.userService,
await this.apiKeyService.clear(); 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() { this.program = new Program(this);
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<string>(ConstantsService.localeKey);
await this.i18nService.init(locale);
this.authService.init();
const installedVersion = await this.storageService.get<string>(ConstantsService.installedVersionKey); async run() {
const currentVersion = await this.platformUtilsService.getApplicationVersion(); await this.init();
if (installedVersion == null || installedVersion !== currentVersion) { this.program.run();
await this.storageService.save(ConstantsService.installedVersionKey, currentVersion); }
}
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<string>(ConstantsService.localeKey);
await this.i18nService.init(locale);
this.authService.init();
const installedVersion = await this.storageService.get<string>(
ConstantsService.installedVersionKey
);
const currentVersion = await this.platformUtilsService.getApplicationVersion();
if (installedVersion == null || installedVersion !== currentVersion) {
await this.storageService.save(ConstantsService.installedVersionKey, currentVersion);
} }
}
} }
const main = new Main(); const main = new Main();

View File

@@ -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 { Response } from "jslib-node/cli/models/response";
import { MessageResponse } from 'jslib-node/cli/models/response/messageResponse'; import { MessageResponse } from "jslib-node/cli/models/response/messageResponse";
export class ClearCacheCommand { export class ClearCacheCommand {
constructor(private configurationService: ConfigurationService, private i18nService: I18nService) { } constructor(
private configurationService: ConfigurationService,
private i18nService: I18nService
) {}
async run(cmd: program.OptionValues): Promise<Response> { async run(cmd: program.OptionValues): Promise<Response> {
try { try {
await this.configurationService.clearStatefulSettings(true); await this.configurationService.clearStatefulSettings(true);
const res = new MessageResponse(this.i18nService.t('syncCacheCleared'), null); const res = new MessageResponse(this.i18nService.t("syncCacheCleared"), null);
return Response.success(res); return Response.success(res);
} catch (e) { } catch (e) {
return Response.error(e); return Response.error(e);
}
} }
}
} }

View File

@@ -1,150 +1,160 @@
import * as program from 'commander'; import * as program from "commander";
import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; import { EnvironmentService } from "jslib-common/abstractions/environment.service";
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 { DirectoryType } from '../enums/directoryType'; import { DirectoryType } from "../enums/directoryType";
import { Response } from 'jslib-node/cli/models/response'; import { Response } from "jslib-node/cli/models/response";
import { MessageResponse } from 'jslib-node/cli/models/response/messageResponse'; import { MessageResponse } from "jslib-node/cli/models/response/messageResponse";
import { AzureConfiguration } from '../models/azureConfiguration'; import { AzureConfiguration } from "../models/azureConfiguration";
import { GSuiteConfiguration } from '../models/gsuiteConfiguration'; import { GSuiteConfiguration } from "../models/gsuiteConfiguration";
import { LdapConfiguration } from '../models/ldapConfiguration'; import { LdapConfiguration } from "../models/ldapConfiguration";
import { OktaConfiguration } from '../models/oktaConfiguration'; import { OktaConfiguration } from "../models/oktaConfiguration";
import { OneLoginConfiguration } from '../models/oneLoginConfiguration'; import { OneLoginConfiguration } from "../models/oneLoginConfiguration";
import { SyncConfiguration } from '../models/syncConfiguration'; 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 { export class ConfigCommand {
private directory: DirectoryType; private directory: DirectoryType;
private ldap = new LdapConfiguration(); private ldap = new LdapConfiguration();
private gsuite = new GSuiteConfiguration(); private gsuite = new GSuiteConfiguration();
private azure = new AzureConfiguration(); private azure = new AzureConfiguration();
private okta = new OktaConfiguration(); private okta = new OktaConfiguration();
private oneLogin = new OneLoginConfiguration(); private oneLogin = new OneLoginConfiguration();
private sync = new SyncConfiguration(); private sync = new SyncConfiguration();
constructor(private environmentService: EnvironmentService, private i18nService: I18nService, constructor(
private configurationService: ConfigurationService) { } private environmentService: EnvironmentService,
private i18nService: I18nService,
private configurationService: ConfigurationService
) {}
async run(setting: string, value: string, options: program.OptionValues): Promise<Response> { async run(setting: string, value: string, options: program.OptionValues): Promise<Response> {
setting = setting.toLowerCase(); setting = setting.toLowerCase();
if (value == null || value === '') { if (value == null || value === "") {
if (options.secretfile) { if (options.secretfile) {
value = await NodeUtils.readFirstLine(options.secretfile); value = await NodeUtils.readFirstLine(options.secretfile);
} else if (options.secretenv && process.env[options.secretenv]) { } else if (options.secretenv && process.env[options.secretenv]) {
value = 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);
} }
try {
private async setServer(url: string) { switch (setting) {
url = (url === 'null' || url === 'bitwarden.com' || url === 'https://bitwarden.com' ? null : url); case "server":
await this.environmentService.setUrls({ await this.setServer(value);
base: url, 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) { private async setServer(url: string) {
const dir = parseInt(type, null); url = url === "null" || url === "bitwarden.com" || url === "https://bitwarden.com" ? null : url;
if (dir < DirectoryType.Ldap || dir > DirectoryType.OneLogin) { await this.environmentService.setUrls({
throw new Error('Invalid directory type value.'); base: url,
} });
await this.loadConfig(); }
this.directory = dir;
await this.saveConfig();
}
private async setLdapPassword(password: string) { private async setDirectory(type: string) {
await this.loadConfig(); const dir = parseInt(type, null);
this.ldap.password = password; if (dir < DirectoryType.Ldap || dir > DirectoryType.OneLogin) {
await this.saveConfig(); throw new Error("Invalid directory type value.");
} }
await this.loadConfig();
this.directory = dir;
await this.saveConfig();
}
private async setGSuiteKey(key: string) { private async setLdapPassword(password: string) {
await this.loadConfig(); await this.loadConfig();
this.gsuite.privateKey = key != null ? key.trimLeft() : null; this.ldap.password = password;
await this.saveConfig(); await this.saveConfig();
} }
private async setAzureKey(key: string) { private async setGSuiteKey(key: string) {
await this.loadConfig(); await this.loadConfig();
this.azure.key = key; this.gsuite.privateKey = key != null ? key.trimLeft() : null;
await this.saveConfig(); await this.saveConfig();
} }
private async setOktaToken(token: string) { private async setAzureKey(key: string) {
await this.loadConfig(); await this.loadConfig();
this.okta.token = token; this.azure.key = key;
await this.saveConfig(); await this.saveConfig();
} }
private async setOneLoginSecret(secret: string) { private async setOktaToken(token: string) {
await this.loadConfig(); await this.loadConfig();
this.oneLogin.clientSecret = secret; this.okta.token = token;
await this.saveConfig(); await this.saveConfig();
} }
private async loadConfig() { private async setOneLoginSecret(secret: string) {
this.directory = await this.configurationService.getDirectoryType(); await this.loadConfig();
this.ldap = (await this.configurationService.getDirectory<LdapConfiguration>(DirectoryType.Ldap)) || this.oneLogin.clientSecret = secret;
this.ldap; await this.saveConfig();
this.gsuite = (await this.configurationService.getDirectory<GSuiteConfiguration>(DirectoryType.GSuite)) || }
this.gsuite;
this.azure = (await this.configurationService.getDirectory<AzureConfiguration>(
DirectoryType.AzureActiveDirectory)) || this.azure;
this.okta = (await this.configurationService.getDirectory<OktaConfiguration>(
DirectoryType.Okta)) || this.okta;
this.oneLogin = (await this.configurationService.getDirectory<OneLoginConfiguration>(
DirectoryType.OneLogin)) || this.oneLogin;
this.sync = (await this.configurationService.getSync()) || this.sync;
}
private async saveConfig() { private async loadConfig() {
ConnectorUtils.adjustConfigForSave(this.ldap, this.sync); this.directory = await this.configurationService.getDirectoryType();
await this.configurationService.saveDirectoryType(this.directory); this.ldap =
await this.configurationService.saveDirectory(DirectoryType.Ldap, this.ldap); (await this.configurationService.getDirectory<LdapConfiguration>(DirectoryType.Ldap)) ||
await this.configurationService.saveDirectory(DirectoryType.GSuite, this.gsuite); this.ldap;
await this.configurationService.saveDirectory(DirectoryType.AzureActiveDirectory, this.azure); this.gsuite =
await this.configurationService.saveDirectory(DirectoryType.Okta, this.okta); (await this.configurationService.getDirectory<GSuiteConfiguration>(DirectoryType.GSuite)) ||
await this.configurationService.saveDirectory(DirectoryType.OneLogin, this.oneLogin); this.gsuite;
await this.configurationService.saveSync(this.sync); this.azure =
} (await this.configurationService.getDirectory<AzureConfiguration>(
DirectoryType.AzureActiveDirectory
)) || this.azure;
this.okta =
(await this.configurationService.getDirectory<OktaConfiguration>(DirectoryType.Okta)) ||
this.okta;
this.oneLogin =
(await this.configurationService.getDirectory<OneLoginConfiguration>(
DirectoryType.OneLogin
)) || this.oneLogin;
this.sync = (await this.configurationService.getSync()) || this.sync;
}
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);
}
} }

View File

@@ -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 { Response } from "jslib-node/cli/models/response";
import { StringResponse } from 'jslib-node/cli/models/response/stringResponse'; import { StringResponse } from "jslib-node/cli/models/response/stringResponse";
export class LastSyncCommand { export class LastSyncCommand {
constructor(private configurationService: ConfigurationService) { } constructor(private configurationService: ConfigurationService) {}
async run(object: string): Promise<Response> { async run(object: string): Promise<Response> {
try { try {
switch (object.toLowerCase()) { switch (object.toLowerCase()) {
case 'groups': case "groups":
const groupsDate = await this.configurationService.getLastGroupSyncDate(); const groupsDate = await this.configurationService.getLastGroupSyncDate();
const groupsRes = new StringResponse(groupsDate == null ? null : groupsDate.toISOString()); const groupsRes = new StringResponse(
return Response.success(groupsRes); groupsDate == null ? null : groupsDate.toISOString()
case 'users': );
const usersDate = await this.configurationService.getLastUserSyncDate(); return Response.success(groupsRes);
const usersRes = new StringResponse(usersDate == null ? null : usersDate.toISOString()); case "users":
return Response.success(usersRes); const usersDate = await this.configurationService.getLastUserSyncDate();
default: const usersRes = new StringResponse(usersDate == null ? null : usersDate.toISOString());
return Response.badRequest('Unknown object.'); return Response.success(usersRes);
} default:
} catch (e) { return Response.badRequest("Unknown object.");
return Response.error(e); }
} } catch (e) {
return Response.error(e);
} }
}
} }

View File

@@ -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 { Response } from "jslib-node/cli/models/response";
import { MessageResponse } from 'jslib-node/cli/models/response/messageResponse'; import { MessageResponse } from "jslib-node/cli/models/response/messageResponse";
export class SyncCommand { export class SyncCommand {
constructor(private syncService: SyncService, private i18nService: I18nService) { } constructor(private syncService: SyncService, private i18nService: I18nService) {}
async run(): Promise<Response> { async run(): Promise<Response> {
try { try {
const result = await this.syncService.sync(false, false); const result = await this.syncService.sync(false, false);
const groupCount = result[0] != null ? result[0].length : 0; const groupCount = result[0] != null ? result[0].length : 0;
const userCount = result[1] != null ? result[1].length : 0; const userCount = result[1] != null ? result[1].length : 0;
const res = new MessageResponse(this.i18nService.t('syncingComplete'), const res = new MessageResponse(
this.i18nService.t('syncCounts', groupCount.toString(), userCount.toString())); this.i18nService.t("syncingComplete"),
return Response.success(res); this.i18nService.t("syncCounts", groupCount.toString(), userCount.toString())
} catch (e) { );
return Response.error(e); return Response.success(res);
} } catch (e) {
return Response.error(e);
} }
}
} }

View File

@@ -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 { Response } from "jslib-node/cli/models/response";
import { TestResponse } from '../models/response/testResponse'; import { TestResponse } from "../models/response/testResponse";
export class TestCommand { export class TestCommand {
constructor(private syncService: SyncService, private i18nService: I18nService) { } constructor(private syncService: SyncService, private i18nService: I18nService) {}
async run(cmd: program.OptionValues): Promise<Response> { async run(cmd: program.OptionValues): Promise<Response> {
try { try {
const result = await ConnectorUtils.simulate(this.syncService, this.i18nService, cmd.last || false); const result = await ConnectorUtils.simulate(
const res = new TestResponse(result); this.syncService,
return Response.success(res); this.i18nService,
} catch (e) { cmd.last || false
return Response.error(e); );
} const res = new TestResponse(result);
return Response.success(res);
} catch (e) {
return Response.error(e);
} }
}
} }

View File

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

2
src/global.d.ts vendored
View File

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

View File

@@ -1,16 +1,19 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline'; <meta
img-src 'self' data: *; child-src *; frame-src *; connect-src *;"> http-equiv="Content-Security-Policy"
<meta name="viewport" content="width=device-width, initial-scale=1"> content="default-src 'self'; style-src 'self' 'unsafe-inline';
img-src 'self' data: *; child-src *; frame-src *; connect-src *;"
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Bitwarden Directory Connector</title> <title>Bitwarden Directory Connector</title>
<base href=""> <base href="" />
</head> </head>
<body> <body>
<app-root> <app-root>
<div id="loading"><i class="fa fa-spinner fa-spin fa-3x"></i></div> <div id="loading"><i class="fa fa-spinner fa-spin fa-3x"></i></div>
</app-root> </app-root>
</body> </body>
</html> </html>

View File

@@ -1,109 +1,135 @@
import { app } from 'electron'; import { app } from "electron";
import * as path from 'path'; import * as path from "path";
import { MenuMain } from './main/menu.main'; import { MenuMain } from "./main/menu.main";
import { MessagingMain } from './main/messaging.main'; import { MessagingMain } from "./main/messaging.main";
import { I18nService } from './services/i18n.service'; import { I18nService } from "./services/i18n.service";
import { KeytarStorageListener } from 'jslib-electron/keytarStorageListener'; import { KeytarStorageListener } from "jslib-electron/keytarStorageListener";
import { ElectronLogService } from 'jslib-electron/services/electronLog.service'; import { ElectronLogService } from "jslib-electron/services/electronLog.service";
import { ElectronMainMessagingService } from 'jslib-electron/services/electronMainMessaging.service'; import { ElectronMainMessagingService } from "jslib-electron/services/electronMainMessaging.service";
import { ElectronStorageService } from 'jslib-electron/services/electronStorage.service'; import { ElectronStorageService } from "jslib-electron/services/electronStorage.service";
import { TrayMain } from 'jslib-electron/tray.main'; import { TrayMain } from "jslib-electron/tray.main";
import { UpdaterMain } from 'jslib-electron/updater.main'; import { UpdaterMain } from "jslib-electron/updater.main";
import { WindowMain } from 'jslib-electron/window.main'; import { WindowMain } from "jslib-electron/window.main";
export class Main { export class Main {
logService: ElectronLogService; logService: ElectronLogService;
i18nService: I18nService; i18nService: I18nService;
storageService: ElectronStorageService; storageService: ElectronStorageService;
messagingService: ElectronMainMessagingService; messagingService: ElectronMainMessagingService;
keytarStorageListener: KeytarStorageListener; keytarStorageListener: KeytarStorageListener;
windowMain: WindowMain; windowMain: WindowMain;
messagingMain: MessagingMain; messagingMain: MessagingMain;
menuMain: MenuMain; menuMain: MenuMain;
updaterMain: UpdaterMain; updaterMain: UpdaterMain;
trayMain: TrayMain; trayMain: TrayMain;
constructor() { constructor() {
// Set paths for portable builds // Set paths for portable builds
let appDataPath = null; let appDataPath = null;
if (process.env.BITWARDEN_CONNECTOR_APPDATA_DIR != null) { if (process.env.BITWARDEN_CONNECTOR_APPDATA_DIR != null) {
appDataPath = process.env.BITWARDEN_CONNECTOR_APPDATA_DIR; appDataPath = process.env.BITWARDEN_CONNECTOR_APPDATA_DIR;
} else if (process.platform === 'win32' && process.env.PORTABLE_EXECUTABLE_DIR != null) { } else if (process.platform === "win32" && process.env.PORTABLE_EXECUTABLE_DIR != null) {
appDataPath = path.join(process.env.PORTABLE_EXECUTABLE_DIR, 'bitwarden-connector-appdata'); 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);
} }
bootstrap() { if (appDataPath != null) {
this.keytarStorageListener.init(); app.setPath("userData", appDataPath);
this.windowMain.init().then(async () => { }
await this.i18nService.init(app.getLocale()); app.setPath("logs", path.join(app.getPath("userData"), "logs"));
this.menuMain.init();
this.messagingMain.init();
await this.updaterMain.init();
await this.trayMain.init(this.i18nService.t('bitwardenDirectoryConnector'));
if (!app.isDefaultProtocolClient('bwdc')) { const args = process.argv.slice(1);
app.setAsDefaultProtocolClient('bwdc'); const watch = args.some((val) => val === "--watch");
}
// Process protocol for macOS if (watch) {
app.on('open-url', (event, url) => { // tslint:disable-next-line
event.preventDefault(); require("electron-reload")(__dirname, {});
this.processDeepLink([url]);
});
}, (e: any) => {
// tslint:disable-next-line
console.error(e);
});
} }
private processDeepLink(argv: string[]): void { this.logService = new ElectronLogService(null, app.getPath("userData"));
argv.filter(s => s.indexOf('bwdc://') === 0).forEach(s => { this.i18nService = new I18nService("en", "./locales/");
const url = new URL(s); this.storageService = new ElectronStorageService(app.getPath("userData"));
const code = url.searchParams.get('code');
const receivedState = url.searchParams.get('state'); this.windowMain = new WindowMain(
if (code != null && receivedState != null) { this.storageService,
this.messagingService.send('ssoCallback', { code: code, state: receivedState }); 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(); const main = new Main();

View File

@@ -1,68 +1,69 @@
import { import { Menu, MenuItem, MenuItemConstructorOptions } from "electron";
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 { export class MenuMain extends BaseMenu {
menu: Menu; menu: Menu;
constructor(private main: Main) { constructor(private main: Main) {
super(main.i18nService, main.windowMain); 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() { (template[template.length - 1].submenu as MenuItemConstructorOptions[]).splice(
this.initProperties(); 1,
this.initContextMenu(); 0,
this.initApplicationMenu(); {
} 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() { this.menu = Menu.buildFromTemplate(template);
const template: MenuItemConstructorOptions[] = [ Menu.setApplicationMenu(this.menu);
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);
}
} }

View File

@@ -1,67 +1,68 @@
import { import { app, ipcMain } from "electron";
app,
ipcMain,
} from 'electron';
import { TrayMain } from 'jslib-electron/tray.main'; import { TrayMain } from "jslib-electron/tray.main";
import { UpdaterMain } from 'jslib-electron/updater.main'; import { UpdaterMain } from "jslib-electron/updater.main";
import { WindowMain } from 'jslib-electron/window.main'; import { WindowMain } from "jslib-electron/window.main";
import { MenuMain } from './menu.main'; import { MenuMain } from "./menu.main";
const SyncCheckInterval = 60 * 1000; // 1 minute const SyncCheckInterval = 60 * 1000; // 1 minute
export class MessagingMain { export class MessagingMain {
private syncTimeout: NodeJS.Timer; private syncTimeout: NodeJS.Timer;
constructor(private windowMain: WindowMain, private menuMain: MenuMain, constructor(
private updaterMain: UpdaterMain, private trayMain: TrayMain) { } private windowMain: WindowMain,
private menuMain: MenuMain,
private updaterMain: UpdaterMain,
private trayMain: TrayMain
) {}
init() { init() {
ipcMain.on('messagingService', async (event: any, message: any) => this.onMessage(message)); ipcMain.on("messagingService", async (event: any, message: any) => this.onMessage(message));
} }
onMessage(message: any) { onMessage(message: any) {
switch (message.command) { switch (message.command) {
case 'checkForUpdate': case "checkForUpdate":
this.updaterMain.checkForUpdate(true); this.updaterMain.checkForUpdate(true);
break; break;
case 'scheduleNextDirSync': case "scheduleNextDirSync":
this.scheduleNextSync(); this.scheduleNextSync();
break; break;
case 'cancelDirSync': case "cancelDirSync":
this.windowMain.win.webContents.send('messagingService', { this.windowMain.win.webContents.send("messagingService", {
command: 'syncScheduleStopped', 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',
}); });
if (this.syncTimeout) { if (this.syncTimeout) {
global.clearTimeout(this.syncTimeout); global.clearTimeout(this.syncTimeout);
} }
break;
this.syncTimeout = global.setTimeout(() => { case "hideToTray":
if (this.windowMain.win == null) { this.trayMain.hideToTray();
return; break;
} default:
break;
this.windowMain.win.webContents.send('messagingService', {
command: 'checkDirSync',
});
}, SyncCheckInterval);
} }
}
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);
}
} }

View File

@@ -1,6 +1,6 @@
export class AzureConfiguration { export class AzureConfiguration {
identityAuthority: string; identityAuthority: string;
tenant: string; tenant: string;
applicationId: string; applicationId: string;
key: string; key: string;
} }

View File

@@ -1,8 +1,8 @@
export abstract class Entry { export abstract class Entry {
referenceId: string; referenceId: string;
externalId: string; externalId: string;
get displayName(): string { get displayName(): string {
return this.referenceId; return this.referenceId;
} }
} }

View File

@@ -1,15 +1,15 @@
import { Entry } from './entry'; import { Entry } from "./entry";
export class GroupEntry extends Entry { export class GroupEntry extends Entry {
name: string; name: string;
userMemberExternalIds = new Set<string>(); userMemberExternalIds = new Set<string>();
groupMemberReferenceIds = new Set<string>(); groupMemberReferenceIds = new Set<string>();
get displayName(): string { get displayName(): string {
if (this.name == null) { if (this.name == null) {
return this.referenceId; return this.referenceId;
}
return this.name;
} }
return this.name;
}
} }

View File

@@ -1,7 +1,7 @@
export class GSuiteConfiguration { export class GSuiteConfiguration {
clientEmail: string; clientEmail: string;
privateKey: string; privateKey: string;
domain: string; domain: string;
adminUser: string; adminUser: string;
customer: string; customer: string;
} }

View File

@@ -1,18 +1,18 @@
export class LdapConfiguration { export class LdapConfiguration {
ssl = false; ssl = false;
startTls = false; startTls = false;
tlsCaPath: string; tlsCaPath: string;
sslAllowUnauthorized = false; sslAllowUnauthorized = false;
sslCertPath: string; sslCertPath: string;
sslKeyPath: string; sslKeyPath: string;
sslCaPath: string; sslCaPath: string;
hostname: string; hostname: string;
port = 389; port = 389;
domain: string; domain: string;
rootPath: string; rootPath: string;
currentUser = false; currentUser = false;
username: string; username: string;
password: string; password: string;
ad = true; ad = true;
pagedSearch = true; pagedSearch = true;
} }

View File

@@ -1,4 +1,4 @@
export class OktaConfiguration { export class OktaConfiguration {
orgUrl: string; orgUrl: string;
token: string; token: string;
} }

View File

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

View File

@@ -1,13 +1,13 @@
import { GroupEntry } from '../groupEntry'; import { GroupEntry } from "../groupEntry";
export class GroupResponse { export class GroupResponse {
externalId: string; externalId: string;
displayName: string; displayName: string;
userIds: string[]; userIds: string[];
constructor(g: GroupEntry) { constructor(g: GroupEntry) {
this.externalId = g.externalId; this.externalId = g.externalId;
this.displayName = g.displayName; this.displayName = g.displayName;
this.userIds = Array.from(g.userMemberExternalIds); this.userIds = Array.from(g.userMemberExternalIds);
} }
} }

View File

@@ -1,22 +1,25 @@
import { GroupResponse } from './groupResponse'; import { GroupResponse } from "./groupResponse";
import { UserResponse } from './userResponse'; 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 { export class TestResponse implements BaseResponse {
object: string; object: string;
groups: GroupResponse[] = []; groups: GroupResponse[] = [];
enabledUsers: UserResponse[] = []; enabledUsers: UserResponse[] = [];
disabledUsers: UserResponse[] = []; disabledUsers: UserResponse[] = [];
deletedUsers: UserResponse[] = []; deletedUsers: UserResponse[] = [];
constructor(result: SimResult) { constructor(result: SimResult) {
this.object = 'test'; this.object = "test";
this.groups = result.groups != null ? result.groups.map(g => new GroupResponse(g)) : []; 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.enabledUsers =
this.disabledUsers = result.disabledUsers != null ? result.disabledUsers.map(u => new UserResponse(u)) : []; result.enabledUsers != null ? result.enabledUsers.map((u) => new UserResponse(u)) : [];
this.deletedUsers = result.deletedUsers != null ? result.deletedUsers.map(u => new UserResponse(u)) : []; this.disabledUsers =
} result.disabledUsers != null ? result.disabledUsers.map((u) => new UserResponse(u)) : [];
this.deletedUsers =
result.deletedUsers != null ? result.deletedUsers.map((u) => new UserResponse(u)) : [];
}
} }

View File

@@ -1,11 +1,11 @@
import { UserEntry } from '../userEntry'; import { UserEntry } from "../userEntry";
export class UserResponse { export class UserResponse {
externalId: string; externalId: string;
displayName: string; displayName: string;
constructor(u: UserEntry) { constructor(u: UserEntry) {
this.externalId = u.externalId; this.externalId = u.externalId;
this.displayName = u.displayName; this.displayName = u.displayName;
} }
} }

View File

@@ -1,10 +1,10 @@
import { GroupEntry } from './groupEntry'; import { GroupEntry } from "./groupEntry";
import { UserEntry } from './userEntry'; import { UserEntry } from "./userEntry";
export class SimResult { export class SimResult {
groups: GroupEntry[] = []; groups: GroupEntry[] = [];
users: UserEntry[] = []; users: UserEntry[] = [];
enabledUsers: UserEntry[] = []; enabledUsers: UserEntry[] = [];
disabledUsers: UserEntry[] = []; disabledUsers: UserEntry[] = [];
deletedUsers: UserEntry[] = []; deletedUsers: UserEntry[] = [];
} }

View File

@@ -1,23 +1,23 @@
export class SyncConfiguration { export class SyncConfiguration {
users = false; users = false;
groups = false; groups = false;
interval = 5; interval = 5;
userFilter: string; userFilter: string;
groupFilter: string; groupFilter: string;
removeDisabled = false; removeDisabled = false;
overwriteExisting = false; overwriteExisting = false;
largeImport = false; largeImport = false;
// Ldap properties // Ldap properties
groupObjectClass: string; groupObjectClass: string;
userObjectClass: string; userObjectClass: string;
groupPath: string; groupPath: string;
userPath: string; userPath: string;
groupNameAttribute: string; groupNameAttribute: string;
userEmailAttribute: string; userEmailAttribute: string;
memberAttribute: string; memberAttribute: string;
useEmailPrefixSuffix = false; useEmailPrefixSuffix = false;
emailPrefixAttribute: string; emailPrefixAttribute: string;
emailSuffix: string; emailSuffix: string;
creationDateAttribute: string; creationDateAttribute: string;
revisionDateAttribute: string; revisionDateAttribute: string;
} }

View File

@@ -1,15 +1,15 @@
import { Entry } from './entry'; import { Entry } from "./entry";
export class UserEntry extends Entry { export class UserEntry extends Entry {
email: string; email: string;
disabled = false; disabled = false;
deleted = false; deleted = false;
get displayName(): string { get displayName(): string {
if (this.email == null) { if (this.email == null) {
return this.referenceId; return this.referenceId;
}
return this.email;
} }
return this.email;
}
} }

View File

@@ -1,302 +1,330 @@
import * as chalk from 'chalk'; import * as chalk from "chalk";
import * as program from 'commander'; import * as program from "commander";
import * as path from 'path'; import * as path from "path";
import { Main } from './bwdc'; import { Main } from "./bwdc";
import { ClearCacheCommand } from './commands/clearCache.command'; import { ClearCacheCommand } from "./commands/clearCache.command";
import { ConfigCommand } from './commands/config.command'; import { ConfigCommand } from "./commands/config.command";
import { LastSyncCommand } from './commands/lastSync.command'; import { LastSyncCommand } from "./commands/lastSync.command";
import { SyncCommand } from './commands/sync.command'; import { SyncCommand } from "./commands/sync.command";
import { TestCommand } from './commands/test.command'; import { TestCommand } from "./commands/test.command";
import { LoginCommand } from 'jslib-node/cli/commands/login.command'; import { LoginCommand } from "jslib-node/cli/commands/login.command";
import { LogoutCommand } from 'jslib-node/cli/commands/logout.command'; import { LogoutCommand } from "jslib-node/cli/commands/logout.command";
import { UpdateCommand } from 'jslib-node/cli/commands/update.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 { ApiKeyService } from "jslib-common/abstractions/apiKey.service";
import { Response } from 'jslib-node/cli/models/response'; import { Response } from "jslib-node/cli/models/response";
import { StringResponse } from 'jslib-node/cli/models/response/stringResponse'; 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 writeLn = (s: string, finalLine: boolean = false, error: boolean = false) => {
const stream = error ? process.stderr : process.stdout; const stream = error ? process.stderr : process.stdout;
if (finalLine && process.platform === 'win32') { if (finalLine && process.platform === "win32") {
stream.write(s); stream.write(s);
} else { } else {
stream.write(s + '\n'); stream.write(s + "\n");
} }
}; };
export class Program extends BaseProgram { export class Program extends BaseProgram {
private apiKeyService: ApiKeyService; private apiKeyService: ApiKeyService;
constructor(private main: Main) { constructor(private main: Main) {
super(main.userService, writeLn); super(main.userService, writeLn);
this.apiKeyService = main.apiKeyService; this.apiKeyService = main.apiKeyService;
} }
async run() { async run() {
program program
.option('--pretty', 'Format output. JSON is tabbed with two spaces.') .option("--pretty", "Format output. JSON is tabbed with two spaces.")
.option('--raw', 'Return raw output instead of a descriptive message.') .option("--raw", "Return raw output instead of a descriptive message.")
.option('--response', 'Return a JSON formatted version of response output.') .option("--response", "Return a JSON formatted version of response output.")
.option('--cleanexit', 'Exit with a success exit code (0) unless an error is thrown.') .option("--cleanexit", "Exit with a success exit code (0) unless an error is thrown.")
.option('--quiet', 'Don\'t return anything to stdout.') .option("--quiet", "Don't return anything to stdout.")
.option('--nointeraction', 'Do not prompt for interactive user input.') .option("--nointeraction", "Do not prompt for interactive user input.")
.version(await this.main.platformUtilsService.getApplicationVersion(), '-v, --version'); .version(await this.main.platformUtilsService.getApplicationVersion(), "-v, --version");
program.on('option:pretty', () => { program.on("option:pretty", () => {
process.env.BW_PRETTY = 'true'; process.env.BW_PRETTY = "true";
}); });
program.on('option:raw', () => { program.on("option:raw", () => {
process.env.BW_RAW = 'true'; process.env.BW_RAW = "true";
}); });
program.on('option:quiet', () => { program.on("option:quiet", () => {
process.env.BW_QUIET = 'true'; process.env.BW_QUIET = "true";
}); });
program.on('option:response', () => { program.on("option:response", () => {
process.env.BW_RESPONSE = 'true'; process.env.BW_RESPONSE = "true";
}); });
program.on('option:cleanexit', () => { program.on("option:cleanexit", () => {
process.env.BW_CLEANEXIT = 'true'; process.env.BW_CLEANEXIT = "true";
}); });
program.on('option:nointeraction', () => { program.on("option:nointeraction", () => {
process.env.BW_NOINTERACTION = 'true'; process.env.BW_NOINTERACTION = "true";
}); });
program.on('command:*', () => { program.on("command:*", () => {
writeLn(chalk.redBright('Invalid command: ' + program.args.join(' ')), false, true); writeLn(chalk.redBright("Invalid command: " + program.args.join(" ")), false, true);
writeLn('See --help for a list of available commands.', true, true); writeLn("See --help for a list of available commands.", true, true);
process.exitCode = 1; process.exitCode = 1;
}); });
program.on('--help', () => { program.on("--help", () => {
writeLn('\n Examples:'); writeLn("\n Examples:");
writeLn(''); writeLn("");
writeLn(' bwdc login'); writeLn(" bwdc login");
writeLn(' bwdc test'); writeLn(" bwdc test");
writeLn(' bwdc sync'); writeLn(" bwdc sync");
writeLn(' bwdc last-sync'); writeLn(" bwdc last-sync");
writeLn(' bwdc config server https://bw.company.com'); writeLn(" bwdc config server https://bw.company.com");
writeLn(' bwdc update'); writeLn(" bwdc update");
writeLn('', true); writeLn("", true);
}); });
program program
.command('login [clientId] [clientSecret]') .command("login [clientId] [clientSecret]")
.description('Log into an organization account.', { .description("Log into an organization account.", {
clientId: 'Client_id part of your organization\'s API key', clientId: "Client_id part of your organization's API key",
clientSecret: 'Client_secret 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) => { .action(async (clientId: string, clientSecret: string, options: program.OptionValues) => {
await this.exitIfAuthed(); await this.exitIfAuthed();
const command = new LoginCommand(this.main.authService, this.main.apiService, this.main.i18nService, const command = new LoginCommand(
this.main.environmentService, this.main.passwordGenerationService, this.main.cryptoFunctionService, this.main.authService,
this.main.platformUtilsService, this.main.userService, this.main.cryptoService, this.main.apiService,
this.main.policyService, 'connector', this.main.loginSyncService, this.main.keyConnectorService); 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)) { if (!Utils.isNullOrWhitespace(clientId)) {
process.env.BW_CLIENTID = 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 <object>')
.description('Get the last successful sync date.')
.on('--help', () => {
writeLn('\n Notes:');
writeLn('');
writeLn(' Returns empty response if no sync has been performed for the given object.');
writeLn('');
writeLn(' Examples:');
writeLn('');
writeLn(' bwdc last-sync groups');
writeLn(' bwdc last-sync users');
writeLn('', true);
})
.action(async (object: string) => {
await this.exitIfNotAuthed();
const command = new LastSyncCommand(this.main.configurationService);
const response = await command.run(object);
this.processResponse(response);
});
program
.command('config <setting> [value]')
.description('Configure settings.')
.option('--secretenv <variable-name>', 'Read secret from the named environment variable.')
.option('--secretfile <filename>', '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 <password>');
writeLn(' bwdc config ldap.password --secretenv LDAP_PWD');
writeLn(' bwdc config azure.key <key>');
writeLn(' bwdc config gsuite.key <key>');
writeLn(' bwdc config okta.token <token>');
writeLn(' bwdc config onelogin.secret <secret>');
writeLn('', true);
})
.action(async (setting: 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(clientSecret)) {
process.env.BW_CLIENTSECRET = clientSecret;
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() { options = Object.assign(options ?? {}, { apikey: true }); // force apikey use
const authed = await this.apiKeyService.isAuthenticated(); const response = await command.run(null, null, options);
if (!authed) { this.processResponse(response);
this.processResponse(Response.error('You are not logged in.'), true); });
}
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 <object>")
.description("Get the last successful sync date.")
.on("--help", () => {
writeLn("\n Notes:");
writeLn("");
writeLn(" Returns empty response if no sync has been performed for the given object.");
writeLn("");
writeLn(" Examples:");
writeLn("");
writeLn(" bwdc last-sync groups");
writeLn(" bwdc last-sync users");
writeLn("", true);
})
.action(async (object: string) => {
await this.exitIfNotAuthed();
const command = new LastSyncCommand(this.main.configurationService);
const response = await command.run(object);
this.processResponse(response);
});
program
.command("config <setting> [value]")
.description("Configure settings.")
.option("--secretenv <variable-name>", "Read secret from the named environment variable.")
.option("--secretfile <filename>", "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 <password>");
writeLn(" bwdc config ldap.password --secretenv LDAP_PWD");
writeLn(" bwdc config azure.key <key>");
writeLn(" bwdc config gsuite.key <key>");
writeLn(" bwdc config okta.token <token>");
writeLn(" bwdc config onelogin.secret <secret>");
writeLn("", true);
})
.action(async (setting: 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);
}
}
} }

View File

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

View File

@@ -1,7 +1,7 @@
@import "~bootstrap/scss/_variables.scss"; @import "~bootstrap/scss/_variables.scss";
html.os_windows { html.os_windows {
body { body {
border-top: 1px solid $gray-400; border-top: 1px solid $gray-400;
} }
} }

View File

@@ -1,144 +1,143 @@
@import "~bootstrap/scss/_variables.scss"; @import "~bootstrap/scss/_variables.scss";
body { body {
padding: 10px 0 20px 0; padding: 10px 0 20px 0;
} }
h1 { h1 {
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
margin-bottom: 20px; margin-bottom: 20px;
small { small {
color: $text-muted; color: $text-muted;
font-size: $h1-font-size * .5; font-size: $h1-font-size * 0.5;
} }
} }
h2 { h2 {
text-transform: uppercase; text-transform: uppercase;
font-weight: bold; font-weight: bold;
} }
h3 { h3 {
text-transform: uppercase; text-transform: uppercase;
} }
h4 { h4 {
font-weight: bold; font-weight: bold;
} }
#duo-frame { #duo-frame {
background: url('../images/loading.svg') 0 0 no-repeat; background: url("../images/loading.svg") 0 0 no-repeat;
height: 380px; height: 380px;
iframe { iframe {
width: 100%; width: 100%;
height: 100%; height: 100%;
border: none; border: none;
} }
} }
app-root > #loading { app-root > #loading {
text-align: center; text-align: center;
margin-top: 20px; margin-top: 20px;
color: $text-muted; color: $text-muted;
} }
ul.testing-list { ul.testing-list {
ul { ul {
padding-left: 18px; padding-left: 18px;
} }
li.deleted { li.deleted {
text-decoration: line-through; text-decoration: line-through;
} }
} }
.callout { .callout {
padding: 10px; padding: 10px;
margin-bottom: 10px; margin-bottom: 10px;
border: 1px solid #000000; border: 1px solid #000000;
border-left-width: 5px; border-left-width: 5px;
border-radius: 3px; border-radius: 3px;
border-color: #ddd; border-color: #ddd;
background-color: white; 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 { .callout-heading {
margin-top: 0; color: $primary;
} }
}
h3.callout-heading { &.callout-info {
font-weight: bold; border-left-color: $info;
text-transform: uppercase;
.callout-heading {
color: $info;
} }
}
&.callout-primary { &.callout-danger {
border-left-color: $primary; border-left-color: $danger;
.callout-heading { .callout-heading {
color: $primary; color: $danger;
}
} }
}
&.callout-info { &.callout-success {
border-left-color: $info; border-left-color: $success;
.callout-heading { .callout-heading {
color: $info; color: $success;
}
} }
}
&.callout-danger { &.callout-warning {
border-left-color: $danger; border-left-color: $warning;
.callout-heading { .callout-heading {
color: $danger; color: $warning;
}
} }
}
&.callout-success { ul {
border-left-color: $success; padding-left: 40px;
margin: 0;
.callout-heading { }
color: $success;
}
}
&.callout-warning {
border-left-color: $warning;
.callout-heading {
color: $warning;
}
}
ul {
padding-left: 40px;
margin: 0;
}
} }
.btn[class*="btn-outline-"] { .btn[class*="btn-outline-"] {
&:not(:hover) { &:not(:hover) {
border-color: $secondary; border-color: $secondary;
background-color: #fbfbfb; background-color: #fbfbfb;
} }
} }
.btn-outline-secondary { .btn-outline-secondary {
color: $text-muted; color: $text-muted;
&:hover:not(:disabled) { &:hover:not(:disabled) {
color: $body-color; color: $body-color;
} }
&:disabled { &:disabled {
opacity: 1; opacity: 1;
} }
&:focus, &:focus,
&.focus { &.focus {
box-shadow: 0 0 0 $btn-focus-width rgba(mix(color-yiq($primary), $primary, 15%), .5); box-shadow: 0 0 0 $btn-focus-width rgba(mix(color-yiq($primary), $primary, 15%), 0.5);
} }
} }

View File

@@ -1,127 +1,128 @@
$fa-font-path: "~font-awesome/fonts"; $fa-font-path: "~font-awesome/fonts";
@import "~font-awesome/scss/font-awesome.scss"; @import "~font-awesome/scss/font-awesome.scss";
@import '~ngx-toastr/toastr'; @import "~ngx-toastr/toastr";
@import "~bootstrap/scss/_variables.scss"; @import "~bootstrap/scss/_variables.scss";
.toast-container { .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 { .toast-close-button {
font-size: 18px; position: absolute;
margin-right: 4px; right: 5px;
top: 0;
} }
.ngx-toastr { &:hover {
align-items: center; box-shadow: 0 0 10px rgba(0, 0, 0, 0.6);
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";
}
}
} }
.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 { @keyframes modalshow {
0% { 0% {
opacity: 0; opacity: 0;
transform: translate(0, -25%); transform: translate(0, -25%);
} }
100% { 100% {
opacity: 1; opacity: 1;
transform: translate(0, 0); transform: translate(0, 0);
} }
} }
@keyframes backdropshow { @keyframes backdropshow {
0% { 0% {
opacity: 0; opacity: 0;
} }
100% { 100% {
opacity: $modal-backdrop-opacity; opacity: $modal-backdrop-opacity;
} }
} }
.modal { .modal {
display: block !important; display: block !important;
opacity: 1 !important; opacity: 1 !important;
} }
.modal-dialog { .modal-dialog {
.modal.fade & { .modal.fade & {
transform: initial !important; transform: initial !important;
animation: modalshow 0.3s ease-in; animation: modalshow 0.3s ease-in;
} }
.modal.show & { .modal.show & {
transform: initial !important; transform: initial !important;
} }
transform: translate(0, 0); transform: translate(0, 0);
} }
.modal-backdrop { .modal-backdrop {
&.fade { &.fade {
animation: backdropshow 0.1s ease-in; animation: backdropshow 0.1s ease-in;
} }
opacity: $modal-backdrop-opacity !important; opacity: $modal-backdrop-opacity !important;
} }

View File

@@ -1,31 +1,36 @@
import { ApiKeyService } from 'jslib-common/abstractions/apiKey.service'; import { ApiKeyService } from "jslib-common/abstractions/apiKey.service";
import { AuthService } from 'jslib-common/abstractions/auth.service'; import { AuthService } from "jslib-common/abstractions/auth.service";
import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { TokenService } from 'jslib-common/abstractions/token.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) { export async function refreshToken(apiKeyService: ApiKeyService, authService: AuthService) {
try { try {
const clientId = await apiKeyService.getClientId(); const clientId = await apiKeyService.getClientId();
const clientSecret = await apiKeyService.getClientSecret(); const clientSecret = await apiKeyService.getClientSecret();
if (clientId != null && clientSecret != null) { if (clientId != null && clientSecret != null) {
await authService.logInApiKey(clientId, clientSecret); await authService.logInApiKey(clientId, clientSecret);
}
} catch (e) {
return Promise.reject(e);
} }
} catch (e) {
return Promise.reject(e);
}
} }
export class ApiService extends ApiServiceBase { export class ApiService extends ApiServiceBase {
constructor(tokenService: TokenService, platformUtilsService: PlatformUtilsService, environmentService: EnvironmentService, constructor(
private refreshTokenCallback: () => Promise<void>, logoutCallback: (expired: boolean) => Promise<void>, tokenService: TokenService,
customUserAgent: string = null) { platformUtilsService: PlatformUtilsService,
super(tokenService, platformUtilsService, environmentService, logoutCallback, customUserAgent); environmentService: EnvironmentService,
} private refreshTokenCallback: () => Promise<void>,
logoutCallback: (expired: boolean) => Promise<void>,
customUserAgent: string = null
) {
super(tokenService, platformUtilsService, environmentService, logoutCallback, customUserAgent);
}
doRefreshToken(): Promise<void> { doRefreshToken(): Promise<void> {
return this.refreshTokenCallback(); return this.refreshTokenCallback();
} }
} }

View File

@@ -1,66 +1,96 @@
import { ApiService } from 'jslib-common/abstractions/api.service'; import { ApiService } from "jslib-common/abstractions/api.service";
import { ApiKeyService } from 'jslib-common/abstractions/apiKey.service'; import { ApiKeyService } from "jslib-common/abstractions/apiKey.service";
import { AppIdService } from 'jslib-common/abstractions/appId.service'; import { AppIdService } from "jslib-common/abstractions/appId.service";
import { CryptoService } from 'jslib-common/abstractions/crypto.service'; import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { CryptoFunctionService } from 'jslib-common/abstractions/cryptoFunction.service'; import { CryptoFunctionService } from "jslib-common/abstractions/cryptoFunction.service";
import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { KeyConnectorService } from 'jslib-common/abstractions/keyConnector.service'; import { KeyConnectorService } from "jslib-common/abstractions/keyConnector.service";
import { LogService } from 'jslib-common/abstractions/log.service'; import { LogService } from "jslib-common/abstractions/log.service";
import { MessagingService } from 'jslib-common/abstractions/messaging.service'; import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { TokenService } from 'jslib-common/abstractions/token.service'; import { TokenService } from "jslib-common/abstractions/token.service";
import { UserService } from 'jslib-common/abstractions/user.service'; import { UserService } from "jslib-common/abstractions/user.service";
import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.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 { AuthResult } from "jslib-common/models/domain/authResult";
import { DeviceRequest } from 'jslib-common/models/request/deviceRequest'; import { DeviceRequest } from "jslib-common/models/request/deviceRequest";
import { TokenRequest } from 'jslib-common/models/request/tokenRequest'; import { TokenRequest } from "jslib-common/models/request/tokenRequest";
import { IdentityTokenResponse } from 'jslib-common/models/response/identityTokenResponse'; import { IdentityTokenResponse } from "jslib-common/models/response/identityTokenResponse";
export class AuthService extends AuthServiceBase { 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, async logInApiKey(clientId: string, clientSecret: string): Promise<AuthResult> {
tokenService: TokenService, appIdService: AppIdService, i18nService: I18nService, this.selectedTwoFactorProviderType = null;
platformUtilsService: PlatformUtilsService, messagingService: MessagingService, if (clientId.startsWith("organization")) {
vaultTimeoutService: VaultTimeoutService, logService: LogService, private apiKeyService: ApiKeyService, return await this.organizationLogInHelper(clientId, clientSecret);
cryptoFunctionService: CryptoFunctionService, environmentService: EnvironmentService,
keyConnectorService: KeyConnectorService) {
super(cryptoService, apiService, userService, tokenService, appIdService, i18nService, platformUtilsService,
messagingService, vaultTimeoutService, logService, cryptoFunctionService, environmentService,
keyConnectorService, false);
} }
return await super.logInApiKey(clientId, clientSecret);
}
async logInApiKey(clientId: string, clientSecret: string): Promise<AuthResult> { async logOut(callback: Function) {
this.selectedTwoFactorProviderType = null; this.apiKeyService.clear();
if (clientId.startsWith('organization')) { super.logOut(callback);
return await this.organizationLogInHelper(clientId, clientSecret); }
}
return await super.logInApiKey(clientId, clientSecret);
}
async logOut(callback: Function) { private async organizationLogInHelper(clientId: string, clientSecret: string) {
this.apiKeyService.clear(); const appId = await this.appIdService.getAppId();
super.logOut(callback); 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 response = await this.apiService.postIdentityToken(request);
const appId = await this.appIdService.getAppId(); const result = new AuthResult();
const deviceRequest = new DeviceRequest(appId, this.platformUtilsService); result.twoFactor = !(response as any).accessToken;
const request = new TokenRequest(null, null, [clientId, clientSecret], null,
null, false, null, deviceRequest);
const response = await this.apiService.postIdentityToken(request); const tokenResponse = response as IdentityTokenResponse;
const result = new AuthResult(); result.resetMasterPassword = tokenResponse.resetMasterPassword;
result.twoFactor = !(response as any).accessToken; await this.tokenService.setToken(tokenResponse.accessToken);
await this.apiKeyService.setInformation(clientId, clientSecret);
const tokenResponse = response as IdentityTokenResponse; return result;
result.resetMasterPassword = tokenResponse.resetMasterPassword; }
await this.tokenService.setToken(tokenResponse.accessToken);
await this.apiKeyService.setInformation(clientId, clientSecret);
return result;
}
} }

View File

@@ -1,473 +1,500 @@
import * as graph from '@microsoft/microsoft-graph-client'; import * as graph from "@microsoft/microsoft-graph-client";
import * as graphType from '@microsoft/microsoft-graph-types'; import * as graphType from "@microsoft/microsoft-graph-types";
import * as https from 'https'; import * as https from "https";
import * as querystring from 'querystring'; import * as querystring from "querystring";
import { DirectoryType } from '../enums/directoryType'; import { DirectoryType } from "../enums/directoryType";
import { AzureConfiguration } from '../models/azureConfiguration'; import { AzureConfiguration } from "../models/azureConfiguration";
import { GroupEntry } from '../models/groupEntry'; import { GroupEntry } from "../models/groupEntry";
import { SyncConfiguration } from '../models/syncConfiguration'; import { SyncConfiguration } from "../models/syncConfiguration";
import { UserEntry } from '../models/userEntry'; import { UserEntry } from "../models/userEntry";
import { BaseDirectoryService } from './baseDirectory.service'; import { BaseDirectoryService } from "./baseDirectory.service";
import { ConfigurationService } from './configuration.service'; import { ConfigurationService } from "./configuration.service";
import { IDirectoryService } from './directory.service'; import { IDirectoryService } from "./directory.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from 'jslib-common/abstractions/log.service'; import { LogService } from "jslib-common/abstractions/log.service";
const AzurePublicIdentityAuhtority = 'login.microsoftonline.com'; const AzurePublicIdentityAuhtority = "login.microsoftonline.com";
const AzureGovermentIdentityAuhtority = 'login.microsoftonline.us'; const AzureGovermentIdentityAuhtority = "login.microsoftonline.us";
const NextLink = '@odata.nextLink'; const NextLink = "@odata.nextLink";
const DeltaLink = '@odata.deltaLink'; const DeltaLink = "@odata.deltaLink";
const ObjectType = '@odata.type'; const ObjectType = "@odata.type";
const UserSelectParams = '?$select=id,mail,userPrincipalName,displayName,accountEnabled'; const UserSelectParams = "?$select=id,mail,userPrincipalName,displayName,accountEnabled";
enum UserSetType { enum UserSetType {
IncludeUser, IncludeUser,
ExcludeUser, ExcludeUser,
IncludeGroup, IncludeGroup,
ExcludeGroup, ExcludeGroup,
} }
export class AzureDirectoryService extends BaseDirectoryService implements IDirectoryService { export class AzureDirectoryService extends BaseDirectoryService implements IDirectoryService {
private client: graph.Client; private client: graph.Client;
private dirConfig: AzureConfiguration; private dirConfig: AzureConfiguration;
private syncConfig: SyncConfiguration; private syncConfig: SyncConfiguration;
private accessToken: string; private accessToken: string;
private accessTokenExpiration: Date; private accessTokenExpiration: Date;
constructor(private configurationService: ConfigurationService, private logService: LogService, constructor(
private i18nService: I18nService) { private configurationService: ConfigurationService,
super(); private logService: LogService,
this.init(); 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[]]> { this.dirConfig = await this.configurationService.getDirectory<AzureConfiguration>(
const type = await this.configurationService.getDirectoryType(); DirectoryType.AzureActiveDirectory
if (type !== DirectoryType.AzureActiveDirectory) { );
return; if (this.dirConfig == null) {
} return;
this.dirConfig = await this.configurationService.getDirectory<AzureConfiguration>(
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];
} }
private async getCurrentUsers(): Promise<UserEntry[]> { this.syncConfig = await this.configurationService.getSync();
const entryIds = new Set<string>(); if (this.syncConfig == null) {
const entries: UserEntry[] = []; return;
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;
} }
private async getDeletedUsers(force: boolean, saveDelta: boolean): Promise<UserEntry[]> { let users: UserEntry[];
const entryIds = new Set<string>(); if (this.syncConfig.users) {
const entries: UserEntry[] = []; users = await this.getCurrentUsers();
const deletedUsers = await this.getDeletedUsers(force, !test);
let res: any = null; users = users.concat(deletedUsers);
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<string>]> { let groups: GroupEntry[];
if (filter == null || filter === '') { if (this.syncConfig.groups) {
return null; const setFilter = await this.createAadCustomSet(this.syncConfig.groupFilter);
} groups = await this.getGroups(setFilter);
users = this.filterUsersFromGroupsSet(users, groups, setFilter, this.syncConfig);
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<string>();
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<string>] { return [groups, users];
if (filter == null || filter === '') { }
return null;
}
const mainParts = filter.split('|'); private async getCurrentUsers(): Promise<UserEntry[]> {
if (mainParts.length < 1 || mainParts[0] == null || mainParts[0].trim() === '') { const entryIds = new Set<string>();
return null; 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 (
if (parts.length !== 2) { !entry.disabled &&
return null; !entry.deleted &&
} (entry.email == null || entry.email.indexOf("#") > -1)
) {
continue;
}
const keyword = parts[0].trim().toLowerCase(); entries.push(entry);
let userSetType = UserSetType.IncludeUser; entryIds.add(user.id);
if (keyword === 'include') {
userSetType = UserSetType.IncludeUser;
} else if (keyword === 'exclude') {
userSetType = UserSetType.ExcludeUser;
} else if (keyword === 'includegroup') {
userSetType = UserSetType.IncludeGroup;
} else if (keyword === 'excludegroup') {
userSetType = UserSetType.ExcludeGroup;
} else {
return null;
} }
}
const set = new Set<string>(); if (res[NextLink] == null) {
const pieces = parts[1].split(','); break;
for (const p of pieces) { } else {
set.add(p.trim().toLowerCase()); const nextReq = this.client.api(res[NextLink]);
} res = await nextReq.get();
}
return [userSetType, set];
} }
private async filterOutUserResult(setFilter: [UserSetType, Set<string>], user: UserEntry, return entries;
checkGroupsFilter: boolean): Promise<boolean> { }
if (setFilter == null) {
return false; private async getDeletedUsers(force: boolean, saveDelta: boolean): Promise<UserEntry[]> {
const entryIds = new Set<string>();
const entries: UserEntry[] = [];
let res: any = null;
const token = await this.configurationService.getUserDeltaToken();
if (!force && token != null) {
try {
const deltaReq = this.client.api(token);
res = await deltaReq.get();
} catch {
res = null;
}
}
if (res == null) {
const userReq = this.client.api("/users/delta" + 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<string>]> {
if (filter == null || filter === "") {
return null;
}
const mainParts = filter.split("|");
if (mainParts.length < 1 || mainParts[0] == null || mainParts[0].trim() === "") {
return null;
}
const parts = mainParts[0].split(":");
if (parts.length !== 2) {
return null;
}
const keyword = parts[0].trim().toLowerCase();
let 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<string>();
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<string>] {
if (filter == null || filter === "") {
return null;
}
const mainParts = filter.split("|");
if (mainParts.length < 1 || mainParts[0] == null || mainParts[0].trim() === "") {
return null;
}
const parts = mainParts[0].split(":");
if (parts.length !== 2) {
return null;
}
const keyword = parts[0].trim().toLowerCase();
let userSetType = UserSetType.IncludeUser;
if (keyword === "include") {
userSetType = UserSetType.IncludeUser;
} else if (keyword === "exclude") {
userSetType = UserSetType.ExcludeUser;
} else if (keyword === "includegroup") {
userSetType = UserSetType.IncludeGroup;
} else if (keyword === "excludegroup") {
userSetType = UserSetType.ExcludeGroup;
} else {
return null;
}
const set = new Set<string>();
const pieces = parts[1].split(",");
for (const p of pieces) {
set.add(p.trim().toLowerCase());
}
return [userSetType, set];
}
private async filterOutUserResult(
setFilter: [UserSetType, Set<string>],
user: UserEntry,
checkGroupsFilter: boolean
): Promise<boolean> {
if (setFilter == null) {
return false;
}
let userSetTypeExclude = null;
if (setFilter[0] === UserSetType.IncludeUser) {
userSetTypeExclude = false;
} else if (setFilter[0] === UserSetType.ExcludeUser) {
userSetTypeExclude = true;
}
if (userSetTypeExclude != null) {
return this.filterOutResult([userSetTypeExclude, setFilter[1]], user.email);
}
// 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<string>]): Promise<GroupEntry[]> {
const entryIds = new Set<string>();
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<GroupEntry> {
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; const identityAuthority =
if (setFilter[0] === UserSetType.IncludeUser) { this.dirConfig.identityAuthority != null
userSetTypeExclude = false; ? this.dirConfig.identityAuthority
} else if (setFilter[0] === UserSetType.ExcludeUser) { : AzurePublicIdentityAuhtority;
userSetTypeExclude = true; if (
identityAuthority !== AzurePublicIdentityAuhtority &&
identityAuthority !== AzureGovermentIdentityAuhtority
) {
done(new Error(this.i18nService.t("dirConfigIncomplete")), null);
return;
} }
if (userSetTypeExclude != null) { if (!this.accessTokenIsExpired()) {
return this.filterOutResult([userSetTypeExclude, setFilter[1]], user.email); done(null, this.accessToken);
return;
} }
// We need to *not* call the /checkMemberGroups method for deleted users, it will always fail this.accessToken = null;
if (!checkGroupsFilter) { this.accessTokenExpiration = null;
return false;
} const data = querystring.stringify({
const memberGroups = await this.client.api(`/users/${user.externalId}/checkMemberGroups`).post({ client_id: this.dirConfig.applicationId,
groupIds: Array.from(setFilter[1]), 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; const req = https
} .request(
{
private buildUser(user: graphType.User): UserEntry { host: identityAuthority,
const entry = new UserEntry(); path: "/" + this.dirConfig.tenant + "/oauth2/v2.0/token",
entry.referenceId = user.id; method: "POST",
entry.externalId = user.id; headers: {
entry.email = user.mail; "Content-Type": "application/x-www-form-urlencoded",
"Content-Length": Buffer.byteLength(data),
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<string>]): Promise<GroupEntry[]> {
const entryIds = new Set<string>();
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<GroupEntry> {
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();
}, },
}); (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() { // expired if less than 2 minutes til expiration
if (this.accessToken == null || this.accessTokenExpiration == null) { const now = new Date();
return true; return this.accessTokenExpiration.getTime() - now.getTime() < 120000;
} }
// expired if less than 2 minutes til expiration private setAccessTokenExpiration(accessToken: string, expSeconds: number) {
const now = new Date(); if (accessToken == null || expSeconds == null) {
return this.accessTokenExpiration.getTime() - now.getTime() < 120000; return;
} }
private setAccessTokenExpiration(accessToken: string, expSeconds: number) { this.accessToken = accessToken;
if (accessToken == null || expSeconds == null) { const exp = new Date();
return; exp.setSeconds(exp.getSeconds() + expSeconds);
} this.accessTokenExpiration = exp;
}
this.accessToken = accessToken;
const exp = new Date();
exp.setSeconds(exp.getSeconds() + expSeconds);
this.accessTokenExpiration = exp;
}
} }

View File

@@ -1,91 +1,95 @@
import { SyncConfiguration } from '../models/syncConfiguration'; import { SyncConfiguration } from "../models/syncConfiguration";
import { GroupEntry } from '../models/groupEntry'; import { GroupEntry } from "../models/groupEntry";
import { UserEntry } from '../models/userEntry'; import { UserEntry } from "../models/userEntry";
export abstract class BaseDirectoryService { export abstract class BaseDirectoryService {
protected createDirectoryQuery(filter: string) { protected createDirectoryQuery(filter: string) {
if (filter == null || filter === '') { if (filter == null || filter === "") {
return null; return null;
}
const mainParts = filter.split('|');
if (mainParts.length < 2 || mainParts[1] == null || mainParts[1].trim() === '') {
return null;
}
return mainParts[1].trim();
} }
protected createCustomSet(filter: string): [boolean, Set<string>] { const mainParts = filter.split("|");
if (filter == null || filter === '') { if (mainParts.length < 2 || mainParts[1] == null || mainParts[1].trim() === "") {
return null; 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<string>();
const pieces = parts[1].split(',');
for (const p of pieces) {
set.add(p.trim().toLowerCase());
}
return [exclude, set];
} }
protected filterOutResult(setFilter: [boolean, Set<string>], result: string) { return mainParts[1].trim();
if (setFilter != null) { }
const cleanResult = result != null ? result.trim().toLowerCase() : '--';
const excluded = setFilter[0];
const set = setFilter[1];
if (excluded && set.has(cleanResult)) { protected createCustomSet(filter: string): [boolean, Set<string>] {
return true; if (filter == null || filter === "") {
} else if (!excluded && !set.has(cleanResult)) { return null;
return true;
}
}
return false;
} }
protected filterUsersFromGroupsSet(users: UserEntry[], groups: GroupEntry[], const mainParts = filter.split("|");
setFilter: [boolean, Set<string>], syncConfig: SyncConfiguration): UserEntry[] { if (mainParts.length < 1 || mainParts[0] == null || mainParts[0].trim() === "") {
if (setFilter == null || users == null) { return 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 { const parts = mainParts[0].split(":");
return force || (users != null && users.filter(u => !u.deleted && !u.disabled).length > 0); 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<string>();
const pieces = parts[1].split(",");
for (const p of pieces) {
set.add(p.trim().toLowerCase());
}
return [exclude, set];
}
protected filterOutResult(setFilter: [boolean, Set<string>], 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<string>],
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);
}
} }

View File

@@ -1,229 +1,238 @@
import { DirectoryType } from '../enums/directoryType'; import { DirectoryType } from "../enums/directoryType";
import { StorageService } from 'jslib-common/abstractions/storage.service'; import { StorageService } from "jslib-common/abstractions/storage.service";
import { AzureConfiguration } from '../models/azureConfiguration'; import { AzureConfiguration } from "../models/azureConfiguration";
import { GSuiteConfiguration } from '../models/gsuiteConfiguration'; import { GSuiteConfiguration } from "../models/gsuiteConfiguration";
import { LdapConfiguration } from '../models/ldapConfiguration'; import { LdapConfiguration } from "../models/ldapConfiguration";
import { OktaConfiguration } from '../models/oktaConfiguration'; import { OktaConfiguration } from "../models/oktaConfiguration";
import { OneLoginConfiguration } from '../models/oneLoginConfiguration'; import { OneLoginConfiguration } from "../models/oneLoginConfiguration";
import { SyncConfiguration } from '../models/syncConfiguration'; import { SyncConfiguration } from "../models/syncConfiguration";
const StoredSecurely = '[STORED SECURELY]'; const StoredSecurely = "[STORED SECURELY]";
const Keys = { const Keys = {
ldap: 'ldapPassword', ldap: "ldapPassword",
gsuite: 'gsuitePrivateKey', gsuite: "gsuitePrivateKey",
azure: 'azureKey', azure: "azureKey",
okta: 'oktaToken', okta: "oktaToken",
oneLogin: 'oneLoginClientSecret', oneLogin: "oneLoginClientSecret",
directoryConfigPrefix: 'directoryConfig_', directoryConfigPrefix: "directoryConfig_",
sync: 'syncConfig', sync: "syncConfig",
directoryType: 'directoryType', directoryType: "directoryType",
userDelta: 'userDeltaToken', userDelta: "userDeltaToken",
groupDelta: 'groupDeltaToken', groupDelta: "groupDeltaToken",
lastUserSync: 'lastUserSync', lastUserSync: "lastUserSync",
lastGroupSync: 'lastGroupSync', lastGroupSync: "lastGroupSync",
lastSyncHash: 'lastSyncHash', lastSyncHash: "lastSyncHash",
organizationId: 'organizationId', organizationId: "organizationId",
}; };
export class ConfigurationService { export class ConfigurationService {
constructor(private storageService: StorageService, private secureStorageService: StorageService, constructor(
private useSecureStorageForSecrets = true) { } private storageService: StorageService,
private secureStorageService: StorageService,
private useSecureStorageForSecrets = true
) {}
async getDirectory<T>(type: DirectoryType): Promise<T> { async getDirectory<T>(type: DirectoryType): Promise<T> {
const config = await this.storageService.get<T>(Keys.directoryConfigPrefix + type); const config = await this.storageService.get<T>(Keys.directoryConfigPrefix + type);
if (config == null) { if (config == null) {
return config; return config;
}
if (this.useSecureStorageForSecrets) {
switch (type) {
case DirectoryType.Ldap:
(config as any).password = await this.secureStorageService.get<string>(Keys.ldap);
break;
case DirectoryType.AzureActiveDirectory:
(config as any).key = await this.secureStorageService.get<string>(Keys.azure);
break;
case DirectoryType.Okta:
(config as any).token = await this.secureStorageService.get<string>(Keys.okta);
break;
case DirectoryType.GSuite:
(config as any).privateKey = await this.secureStorageService.get<string>(Keys.gsuite);
break;
case DirectoryType.OneLogin:
(config as any).clientSecret = await this.secureStorageService.get<string>(Keys.oneLogin);
break;
}
}
return config;
} }
async saveDirectory(type: DirectoryType, if (this.useSecureStorageForSecrets) {
config: LdapConfiguration | GSuiteConfiguration | AzureConfiguration | OktaConfiguration | switch (type) {
OneLoginConfiguration): Promise<any> { case DirectoryType.Ldap:
const savedConfig: any = Object.assign({}, config); (config as any).password = await this.secureStorageService.get<string>(Keys.ldap);
if (this.useSecureStorageForSecrets) { break;
switch (type) { case DirectoryType.AzureActiveDirectory:
case DirectoryType.Ldap: (config as any).key = await this.secureStorageService.get<string>(Keys.azure);
if (savedConfig.password == null) { break;
await this.secureStorageService.remove(Keys.ldap); case DirectoryType.Okta:
} else { (config as any).token = await this.secureStorageService.get<string>(Keys.okta);
await this.secureStorageService.save(Keys.ldap, savedConfig.password); break;
savedConfig.password = StoredSecurely; case DirectoryType.GSuite:
} (config as any).privateKey = await this.secureStorageService.get<string>(Keys.gsuite);
break; break;
case DirectoryType.AzureActiveDirectory: case DirectoryType.OneLogin:
if (savedConfig.key == null) { (config as any).clientSecret = await this.secureStorageService.get<string>(Keys.oneLogin);
await this.secureStorageService.remove(Keys.azure); break;
} else { }
await this.secureStorageService.save(Keys.azure, savedConfig.key); }
savedConfig.key = StoredSecurely; return config;
} }
break;
case DirectoryType.Okta: async saveDirectory(
if (savedConfig.token == null) { type: DirectoryType,
await this.secureStorageService.remove(Keys.okta); config:
} else { | LdapConfiguration
await this.secureStorageService.save(Keys.okta, savedConfig.token); | GSuiteConfiguration
savedConfig.token = StoredSecurely; | AzureConfiguration
} | OktaConfiguration
break; | OneLoginConfiguration
case DirectoryType.GSuite: ): Promise<any> {
if (savedConfig.privateKey == null) { const savedConfig: any = Object.assign({}, config);
await this.secureStorageService.remove(Keys.gsuite); if (this.useSecureStorageForSecrets) {
} else { switch (type) {
(config as GSuiteConfiguration).privateKey = savedConfig.privateKey = case DirectoryType.Ldap:
savedConfig.privateKey.replace(/\\n/g, '\n'); if (savedConfig.password == null) {
await this.secureStorageService.save(Keys.gsuite, savedConfig.privateKey); await this.secureStorageService.remove(Keys.ldap);
savedConfig.privateKey = StoredSecurely; } else {
} await this.secureStorageService.save(Keys.ldap, savedConfig.password);
break; savedConfig.password = StoredSecurely;
case DirectoryType.OneLogin: }
if (savedConfig.clientSecret == null) { break;
await this.secureStorageService.remove(Keys.oneLogin); case DirectoryType.AzureActiveDirectory:
} else { if (savedConfig.key == null) {
await this.secureStorageService.save(Keys.oneLogin, savedConfig.clientSecret); await this.secureStorageService.remove(Keys.azure);
savedConfig.clientSecret = StoredSecurely; } else {
} await this.secureStorageService.save(Keys.azure, savedConfig.key);
break; savedConfig.key = StoredSecurely;
} }
} break;
await this.storageService.save(Keys.directoryConfigPrefix + type, savedConfig); 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<SyncConfiguration> {
return this.storageService.get<SyncConfiguration>(Keys.sync);
}
saveSync(config: SyncConfiguration) {
return this.storageService.save(Keys.sync, config);
}
getDirectoryType(): Promise<DirectoryType> {
return this.storageService.get<DirectoryType>(Keys.directoryType);
}
async saveDirectoryType(type: DirectoryType) {
const currentType = await this.getDirectoryType();
if (type !== currentType) {
await this.clearStatefulSettings();
} }
getSync(): Promise<SyncConfiguration> { return this.storageService.save(Keys.directoryType, type);
return this.storageService.get<SyncConfiguration>(Keys.sync); }
getUserDeltaToken(): Promise<string> {
return this.storageService.get<string>(Keys.userDelta);
}
saveUserDeltaToken(token: string) {
if (token == null) {
return this.storageService.remove(Keys.userDelta);
} else {
return this.storageService.save(Keys.userDelta, token);
}
}
getGroupDeltaToken(): Promise<string> {
return this.storageService.get<string>(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<Date> {
const dateString = await this.storageService.get<string>(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<Date> {
const dateString = await this.storageService.get<string>(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<string> {
return this.storageService.get<string>(Keys.lastSyncHash);
}
saveLastSyncHash(hash: string) {
if (hash == null) {
return this.storageService.remove(Keys.lastSyncHash);
} else {
return this.storageService.save(Keys.lastSyncHash, hash);
}
}
getOrganizationId(): Promise<string> {
return this.storageService.get<string>(Keys.organizationId);
}
async saveOrganizationId(id: string) {
const currentId = await this.getOrganizationId();
if (currentId !== id) {
await this.clearStatefulSettings();
} }
saveSync(config: SyncConfiguration) { if (id == null) {
return this.storageService.save(Keys.sync, config); return this.storageService.remove(Keys.organizationId);
} else {
return this.storageService.save(Keys.organizationId, id);
} }
}
getDirectoryType(): Promise<DirectoryType> { async clearStatefulSettings(hashToo = false) {
return this.storageService.get<DirectoryType>(Keys.directoryType); await this.saveUserDeltaToken(null);
} await this.saveGroupDeltaToken(null);
await this.saveLastGroupSyncDate(null);
async saveDirectoryType(type: DirectoryType) { await this.saveLastUserSyncDate(null);
const currentType = await this.getDirectoryType(); if (hashToo) {
if (type !== currentType) { await this.saveLastSyncHash(null);
await this.clearStatefulSettings();
}
return this.storageService.save(Keys.directoryType, type);
}
getUserDeltaToken(): Promise<string> {
return this.storageService.get<string>(Keys.userDelta);
}
saveUserDeltaToken(token: string) {
if (token == null) {
return this.storageService.remove(Keys.userDelta);
} else {
return this.storageService.save(Keys.userDelta, token);
}
}
getGroupDeltaToken(): Promise<string> {
return this.storageService.get<string>(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<Date> {
const dateString = await this.storageService.get<string>(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<Date> {
const dateString = await this.storageService.get<string>(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<string> {
return this.storageService.get<string>(Keys.lastSyncHash);
}
saveLastSyncHash(hash: string) {
if (hash == null) {
return this.storageService.remove(Keys.lastSyncHash);
} else {
return this.storageService.save(Keys.lastSyncHash, hash);
}
}
getOrganizationId(): Promise<string> {
return this.storageService.get<string>(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);
}
} }
}
} }

View File

@@ -1,6 +1,6 @@
import { GroupEntry } from '../models/groupEntry'; import { GroupEntry } from "../models/groupEntry";
import { UserEntry } from '../models/userEntry'; import { UserEntry } from "../models/userEntry";
export interface IDirectoryService { export interface IDirectoryService {
getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]>; getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]>;
} }

View File

@@ -1,248 +1,260 @@
import { JWT } from 'google-auth-library'; import { JWT } from "google-auth-library";
import { import { admin_directory_v1, google } from "googleapis";
admin_directory_v1,
google,
} from 'googleapis';
import { DirectoryType } from '../enums/directoryType'; import { DirectoryType } from "../enums/directoryType";
import { GroupEntry } from '../models/groupEntry'; import { GroupEntry } from "../models/groupEntry";
import { GSuiteConfiguration } from '../models/gsuiteConfiguration'; import { GSuiteConfiguration } from "../models/gsuiteConfiguration";
import { SyncConfiguration } from '../models/syncConfiguration'; import { SyncConfiguration } from "../models/syncConfiguration";
import { UserEntry } from '../models/userEntry'; import { UserEntry } from "../models/userEntry";
import { BaseDirectoryService } from './baseDirectory.service'; import { BaseDirectoryService } from "./baseDirectory.service";
import { ConfigurationService } from './configuration.service'; import { ConfigurationService } from "./configuration.service";
import { IDirectoryService } from './directory.service'; import { IDirectoryService } from "./directory.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from 'jslib-common/abstractions/log.service'; import { LogService } from "jslib-common/abstractions/log.service";
export class GSuiteDirectoryService extends BaseDirectoryService implements IDirectoryService { export class GSuiteDirectoryService extends BaseDirectoryService implements IDirectoryService {
private client: JWT; private client: JWT;
private service: admin_directory_v1.Admin; private service: admin_directory_v1.Admin;
private authParams: any; private authParams: any;
private dirConfig: GSuiteConfiguration; private dirConfig: GSuiteConfiguration;
private syncConfig: SyncConfiguration; private syncConfig: SyncConfiguration;
constructor(private configurationService: ConfigurationService, private logService: LogService, constructor(
private i18nService: I18nService) { private configurationService: ConfigurationService,
super(); private logService: LogService,
this.service = google.admin('directory_v1'); 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[]]> { this.dirConfig = await this.configurationService.getDirectory<GSuiteConfiguration>(
const type = await this.configurationService.getDirectoryType(); DirectoryType.GSuite
if (type !== DirectoryType.GSuite) { );
return; if (this.dirConfig == null) {
} return;
this.dirConfig = await this.configurationService.getDirectory<GSuiteConfiguration>(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];
} }
private async getUsers(): Promise<UserEntry[]> { this.syncConfig = await this.configurationService.getSync();
const entries: UserEntry[] = []; if (this.syncConfig == null) {
const query = this.createDirectoryQuery(this.syncConfig.userFilter); return;
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) { await this.auth();
if ((user.emails == null || user.emails === '') && !deleted) {
return null;
}
const entry = new UserEntry(); let users: UserEntry[] = [];
entry.referenceId = user.id; if (this.syncConfig.users) {
entry.externalId = user.id; users = await this.getUsers();
entry.email = user.primaryEmail != null ? user.primaryEmail.trim().toLowerCase() : null; }
entry.disabled = user.suspended || false;
entry.deleted = deleted; 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<UserEntry[]> {
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<string>],
users: UserEntry[]
): Promise<GroupEntry[]> {
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; 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<string>], users: UserEntry[]): Promise<GroupEntry[]> { return entry;
const entries: GroupEntry[] = []; }
let nextPageToken: string = null;
while (true) { private async auth() {
this.logService.info('Querying groups - nextPageToken:' + nextPageToken); if (
const p = Object.assign({ pageToken: nextPageToken }, this.authParams); this.dirConfig.clientEmail == null ||
const res = await this.service.groups.list(p); this.dirConfig.privateKey == null ||
if (res.status !== 200) { this.dirConfig.adminUser == null ||
throw new Error('Group list API failed: ' + res.statusText); this.dirConfig.domain == null
} ) {
throw new Error(this.i18nService.t("dirConfigIncomplete"));
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[]) { this.client = new google.auth.JWT({
let nextPageToken: string = null; 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(); await this.client.authorize();
entry.referenceId = group.id;
entry.externalId = group.id;
entry.name = group.name;
while (true) { this.authParams = {
const p = Object.assign({ groupKey: group.id, pageToken: nextPageToken }, this.authParams); auth: this.client,
const memRes = await this.service.members.list(p); };
if (memRes.status !== 200) { if (this.dirConfig.domain != null && this.dirConfig.domain.trim() !== "") {
this.logService.warning('Group member list API failed: ' + memRes.statusText); this.authParams.domain = this.dirConfig.domain;
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;
} }
if (this.dirConfig.customer != null && this.dirConfig.customer.trim() !== "") {
private async auth() { this.authParams.customer = this.dirConfig.customer;
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;
}
} }
}
} }

View File

@@ -1,15 +1,18 @@
import * as fs from 'fs'; import * as fs from "fs";
import * as path from 'path'; 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 { export class I18nService extends BaseI18nService {
constructor(systemLanguage: string, localesDirectory: string) { constructor(systemLanguage: string, localesDirectory: string) {
super(systemLanguage, localesDirectory, (formattedLocale: string) => { super(systemLanguage, localesDirectory, (formattedLocale: string) => {
const filePath = path.join(__dirname, this.localesDirectory + '/' + formattedLocale + '/messages.json'); const filePath = path.join(
const localesJson = fs.readFileSync(filePath, 'utf8'); __dirname,
const locales = JSON.parse(localesJson.replace(/^\uFEFF/, '')); // strip the BOM this.localesDirectory + "/" + formattedLocale + "/messages.json"
return Promise.resolve(locales); );
}); const localesJson = fs.readFileSync(filePath, "utf8");
} const locales = JSON.parse(localesJson.replace(/^\uFEFF/, "")); // strip the BOM
return Promise.resolve(locales);
});
}
} }

View File

@@ -1,29 +1,25 @@
import { import { deletePassword, getPassword, setPassword } from "keytar";
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 { export class KeytarSecureStorageService implements StorageService {
constructor(private serviceName: string) { } constructor(private serviceName: string) {}
get<T>(key: string): Promise<T> { get<T>(key: string): Promise<T> {
return getPassword(this.serviceName, key).then(val => { return getPassword(this.serviceName, key).then((val) => {
return JSON.parse(val) as T; return JSON.parse(val) as T;
}); });
} }
async has(key: string): Promise<boolean> { async has(key: string): Promise<boolean> {
return (await this.get(key)) != null; return (await this.get(key)) != null;
} }
save(key: string, obj: any): Promise<any> { save(key: string, obj: any): Promise<any> {
return setPassword(this.serviceName, key, JSON.stringify(obj)); return setPassword(this.serviceName, key, JSON.stringify(obj));
} }
remove(key: string): Promise<any> { remove(key: string): Promise<any> {
return deletePassword(this.serviceName, key); return deletePassword(this.serviceName, key);
} }
} }

View File

@@ -1,450 +1,506 @@
import * as fs from 'fs'; import * as fs from "fs";
import * as ldap from 'ldapjs'; 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 { GroupEntry } from "../models/groupEntry";
import { LdapConfiguration } from '../models/ldapConfiguration'; import { LdapConfiguration } from "../models/ldapConfiguration";
import { SyncConfiguration } from '../models/syncConfiguration'; import { SyncConfiguration } from "../models/syncConfiguration";
import { UserEntry } from '../models/userEntry'; import { UserEntry } from "../models/userEntry";
import { ConfigurationService } from './configuration.service'; import { ConfigurationService } from "./configuration.service";
import { IDirectoryService } from './directory.service'; import { IDirectoryService } from "./directory.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from 'jslib-common/abstractions/log.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; const UserControlAccountDisabled = 2;
export class LdapDirectoryService implements IDirectoryService { export class LdapDirectoryService implements IDirectoryService {
private client: ldap.Client; private client: ldap.Client;
private dirConfig: LdapConfiguration; private dirConfig: LdapConfiguration;
private syncConfig: SyncConfiguration; private syncConfig: SyncConfiguration;
constructor(private configurationService: ConfigurationService, private logService: LogService, constructor(
private i18nService: I18nService) { } private configurationService: ConfigurationService,
private logService: LogService,
private i18nService: I18nService
) {}
async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> { async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> {
const type = await this.configurationService.getDirectoryType(); const type = await this.configurationService.getDirectoryType();
if (type !== DirectoryType.Ldap) { if (type !== DirectoryType.Ldap) {
return; return;
}
this.dirConfig = await this.configurationService.getDirectory<LdapConfiguration>(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];
} }
private async getUsers(force: boolean): Promise<UserEntry[]> { this.dirConfig = await this.configurationService.getDirectory<LdapConfiguration>(
const lastSync = await this.configurationService.getLastUserSyncDate(); DirectoryType.Ldap
let filter = this.buildBaseFilter(this.syncConfig.userObjectClass, this.syncConfig.userFilter); );
filter = this.buildRevisionFilter(filter, force, lastSync); if (this.dirConfig == null) {
return;
const path = this.makeSearchPath(this.syncConfig.userPath);
this.logService.info('User search: ' + path + ' => ' + filter);
const regularUsers = await this.search<UserEntry>(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<UserEntry>(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 { this.syncConfig = await this.configurationService.getSync();
const user = new UserEntry(); if (this.syncConfig == null) {
user.referenceId = searchEntry.objectName; return;
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<GroupEntry[]> { await this.bind();
const entries: GroupEntry[] = [];
const lastSync = await this.configurationService.getLastUserSyncDate(); let users: UserEntry[];
const originalFilter = this.buildBaseFilter(this.syncConfig.groupObjectClass, this.syncConfig.groupFilter); if (this.syncConfig.users) {
let filter = originalFilter; users = await this.getUsers(force);
const revisionFilter = this.buildRevisionFilter(filter, force, lastSync); }
const searchSinceRevision = filter !== revisionFilter;
filter = revisionFilter;
const path = this.makeSearchPath(this.syncConfig.groupPath); let groups: GroupEntry[];
this.logService.info('Group search: ' + path + ' => ' + filter); 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[] = []; await this.unbind();
const initialSearchGroupIds = await this.search<string>(path, filter, (se: any) => { return [groups, users];
groupSearchEntries.push(se); }
return se.objectName;
private async getUsers(force: boolean): Promise<UserEntry[]> {
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<UserEntry>(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<UserEntry>(
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<GroupEntry[]> {
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<string>(path, filter, (se: any) => {
groupSearchEntries.push(se);
return se.objectName;
});
if (searchSinceRevision && initialSearchGroupIds.length === 0) {
return [];
} else if (searchSinceRevision) {
groupSearchEntries = await this.search<string>(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<string, string>();
await this.search<string>(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<string, string>) {
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<T>(
path: string,
filter: string,
processEntry: (searchEntry: any) => T,
controls: ldap.Control[] = []
): Promise<T[]> {
const options: ldap.SearchOptions = {
filter: filter,
scope: "sub",
paged: this.dirConfig.pagedSearch,
};
const entries: T[] = [];
return new Promise<T[]>((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) { res.on("searchEntry", (entry) => {
return []; const e = processEntry(entry);
} else if (searchSinceRevision) { if (e != null) {
groupSearchEntries = await this.search<string>(path, originalFilter, (se: any) => se); entries.push(e);
} }
const userFilter = this.buildBaseFilter(this.syncConfig.userObjectClass, this.syncConfig.userFilter);
const userPath = this.makeSearchPath(this.syncConfig.userPath);
const userIdMap = new Map<string, string>();
await this.search<string>(userPath, userFilter, (se: any) => {
userIdMap.set(se.objectName, this.getExternalId(se, se.objectName));
return se;
}); });
for (const se of groupSearchEntries) { res.on("end", (result) => {
const group = this.buildGroup(se, userIdMap); resolve(entries);
if (group != null) { });
entries.push(group); });
} });
}
private async bind(): Promise<any> {
return new Promise<void>((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) {
return entries; if (
} this.dirConfig.sslCaPath != null &&
this.dirConfig.sslCaPath !== "" &&
private buildGroup(searchEntry: any, userMap: Map<string, string>) { fs.existsSync(this.dirConfig.sslCaPath)
const group = new GroupEntry(); ) {
group.referenceId = searchEntry.objectName; tlsOptions.ca = [fs.readFileSync(this.dirConfig.sslCaPath)];
if (group.referenceId == null) { }
return null; if (
} this.dirConfig.sslCertPath != null &&
this.dirConfig.sslCertPath !== "" &&
group.externalId = this.getExternalId(searchEntry, group.referenceId); fs.existsSync(this.dirConfig.sslCertPath)
) {
group.name = this.getAttr(searchEntry, this.syncConfig.groupNameAttribute); tlsOptions.cert = fs.readFileSync(this.dirConfig.sslCertPath);
if (group.name == null) { }
group.name = this.getAttr(searchEntry, 'cn'); if (
} this.dirConfig.sslKeyPath != null &&
this.dirConfig.sslKeyPath !== "" &&
if (group.name == null) { fs.existsSync(this.dirConfig.sslKeyPath)
return null; ) {
} tlsOptions.key = fs.readFileSync(this.dirConfig.sslKeyPath);
}
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 { } 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 { tlsOptions.checkServerIdentity = this.checkServerIdentityAltNames;
let filter = this.buildObjectClassFilter(objectClass); options.tlsOptions = tlsOptions;
if (subFilter != null && subFilter.trim() !== '') {
filter = '(&' + filter + subFilter + ')';
}
return filter;
}
private buildObjectClassFilter(objectClass: string): string { this.client = ldap.createClient(options);
return '(&(objectClass=' + objectClass + '))';
}
private buildRevisionFilter(baseFilter: string, force: boolean, lastRevisionDate: Date) { const user =
const revisionAttr = this.syncConfig.revisionDateAttribute; this.dirConfig.username == null || this.dirConfig.username.trim() === ""
if (!force && lastRevisionDate != null && revisionAttr != null && revisionAttr.trim() !== '') { ? null
const dateString = lastRevisionDate.toISOString().replace(/[-:T]/g, '').substr(0, 16) + 'Z'; : this.dirConfig.username;
baseFilter = '(&' + baseFilter + '(' + revisionAttr + '>=' + dateString + '))'; 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.startTls && this.dirConfig.ssl) {
if (this.dirConfig.rootPath.toLowerCase().indexOf('dc=') === -1) { this.client.starttls(options.tlsOptions, undefined, (err, res) => {
return pathPrefix; if (err != null) {
} reject(err.message);
if (this.dirConfig.rootPath != null && this.dirConfig.rootPath.trim() !== '') { } else {
const trimmedRootPath = this.dirConfig.rootPath.trim().toLowerCase(); this.client.bind(user, pass, (err2) => {
let path = trimmedRootPath.substr(trimmedRootPath.indexOf('dc=')); if (err2 != null) {
if (pathPrefix != null && pathPrefix.trim() !== '') { reject(err2.message);
path = pathPrefix.trim() + ',' + path; } else {
} resolve();
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<T>(path: string, filter: string, processEntry: (searchEntry: any) => T,
controls: ldap.Control[] = []): Promise<T[]> {
const options: ldap.SearchOptions = {
filter: filter,
scope: 'sub',
paged: this.dirConfig.pagedSearch,
};
const entries: T[] = [];
return new Promise<T[]>((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);
});
}); });
}
}); });
} } else {
this.client.bind(user, pass, (err) => {
private async bind(): Promise<any> { if (err != null) {
return new Promise<void>((resolve, reject) => { reject(err.message);
if (this.dirConfig.hostname == null || this.dirConfig.port == null) { } else {
reject(this.i18nService.t('dirConfigIncomplete')); resolve();
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();
}
});
}
}); });
} }
});
}
private async unbind(): Promise<void> { private async unbind(): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.client.unbind(err => { this.client.unbind((err) => {
if (err != null) { if (err != null) {
reject(err); reject(err);
} else { } else {
resolve(); 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,
};
} }
});
});
}
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);
}
} }

View File

@@ -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 { export class LowdbStorageService extends LowdbStorageServiceBase {
constructor(logService: LogService, defaults?: any, dir?: string, allowCache = false, private requireLock = false) { constructor(
super(logService, defaults, dir, allowCache); logService: LogService,
} defaults?: any,
dir?: string,
allowCache = false,
private requireLock = false
) {
super(logService, defaults, dir, allowCache);
}
protected async lockDbFile<T>(action: () => T): Promise<T> { protected async lockDbFile<T>(action: () => T): Promise<T> {
if (this.requireLock && !Utils.isNullOrWhitespace(this.dataFilePath)) { if (this.requireLock && !Utils.isNullOrWhitespace(this.dataFilePath)) {
this.logService.info('acquiring db file lock'); this.logService.info("acquiring db file lock");
return await lock.lock(this.dataFilePath, { retries: 3 }).then(release => { return await lock.lock(this.dataFilePath, { retries: 3 }).then((release) => {
try { try {
return action(); return action();
} finally { } finally {
release(); release();
}
});
} else {
return action();
} }
});
} else {
return action();
} }
}
} }

View File

@@ -1,17 +1,30 @@
import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { TokenService } from 'jslib-common/abstractions/token.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 { export class NodeApiService extends NodeApiServiceBase {
constructor(tokenService: TokenService, platformUtilsService: PlatformUtilsService, environmentService: EnvironmentService, constructor(
private refreshTokenCallback: () => Promise<void>, logoutCallback: (expired: boolean) => Promise<void>, tokenService: TokenService,
customUserAgent: string = null, apiKeyRefresh: (clientId: string, clientSecret: string) => Promise<any>) { platformUtilsService: PlatformUtilsService,
super(tokenService, platformUtilsService, environmentService, logoutCallback, customUserAgent, apiKeyRefresh); environmentService: EnvironmentService,
} private refreshTokenCallback: () => Promise<void>,
logoutCallback: (expired: boolean) => Promise<void>,
customUserAgent: string = null,
apiKeyRefresh: (clientId: string, clientSecret: string) => Promise<any>
) {
super(
tokenService,
platformUtilsService,
environmentService,
logoutCallback,
customUserAgent,
apiKeyRefresh
);
}
doRefreshToken(): Promise<void> { doRefreshToken(): Promise<void> {
return this.refreshTokenCallback(); return this.refreshTokenCallback();
} }
} }

View File

@@ -1,253 +1,271 @@
import { DirectoryType } from '../enums/directoryType'; import { DirectoryType } from "../enums/directoryType";
import { GroupEntry } from '../models/groupEntry'; import { GroupEntry } from "../models/groupEntry";
import { OktaConfiguration } from '../models/oktaConfiguration'; import { OktaConfiguration } from "../models/oktaConfiguration";
import { SyncConfiguration } from '../models/syncConfiguration'; import { SyncConfiguration } from "../models/syncConfiguration";
import { UserEntry } from '../models/userEntry'; import { UserEntry } from "../models/userEntry";
import { BaseDirectoryService } from './baseDirectory.service'; import { BaseDirectoryService } from "./baseDirectory.service";
import { ConfigurationService } from './configuration.service'; import { ConfigurationService } from "./configuration.service";
import { IDirectoryService } from './directory.service'; import { IDirectoryService } from "./directory.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from 'jslib-common/abstractions/log.service'; import { LogService } from "jslib-common/abstractions/log.service";
import * as https from 'https'; import * as https from "https";
const DelayBetweenBuildGroupCallsInMilliseconds = 500; const DelayBetweenBuildGroupCallsInMilliseconds = 500;
export class OktaDirectoryService extends BaseDirectoryService implements IDirectoryService { export class OktaDirectoryService extends BaseDirectoryService implements IDirectoryService {
private dirConfig: OktaConfiguration; private dirConfig: OktaConfiguration;
private syncConfig: SyncConfiguration; private syncConfig: SyncConfiguration;
private lastBuildGroupCall: number; private lastBuildGroupCall: number;
constructor(private configurationService: ConfigurationService, private logService: LogService, constructor(
private i18nService: I18nService) { private configurationService: ConfigurationService,
super(); 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[]]> { this.dirConfig = await this.configurationService.getDirectory<OktaConfiguration>(
const type = await this.configurationService.getDirectoryType(); DirectoryType.Okta
if (type !== DirectoryType.Okta) { );
return; if (this.dirConfig == null) {
} return;
this.dirConfig = await this.configurationService.getDirectory<OktaConfiguration>(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];
} }
private async getUsers(force: boolean): Promise<UserEntry[]> { this.syncConfig = await this.configurationService.getSync();
const entries: UserEntry[] = []; if (this.syncConfig == null) {
const lastSync = await this.configurationService.getLastUserSyncDate(); return;
const oktaFilter = this.buildOktaFilter(this.syncConfig.userFilter, force, lastSync); }
const setFilter = this.createCustomSet(this.syncConfig.userFilter);
this.logService.info('Querying users.'); if (this.dirConfig.orgUrl == null || this.dirConfig.token == null) {
const usersPromise = this.apiGetMany('users?filter=' + this.encodeUrlParameter(oktaFilter)) throw new Error(this.i18nService.t("dirConfigIncomplete"));
.then((users: any[]) => { }
for (const user of users) {
const entry = this.buildUser(user); let users: UserEntry[];
if (entry != null && !this.filterOutResult(setFilter, entry.email)) { if (this.syncConfig.users) {
entries.push(entry); users = await this.getUsers(force);
} }
let groups: GroupEntry[];
if (this.syncConfig.groups) {
const setFilter = this.createCustomSet(this.syncConfig.groupFilter);
groups = await this.getGroups(this.forceGroup(force, users), setFilter);
users = this.filterUsersFromGroupsSet(users, groups, setFilter, this.syncConfig);
}
return [groups, users];
}
private async getUsers(force: boolean): Promise<UserEntry[]> {
const entries: UserEntry[] = [];
const 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<string>]
): Promise<GroupEntry[]> {
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<GroupEntry> {
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<string, string | string[]>]> {
const u = new URL(url);
return new Promise((resolve) => {
https.get(
{
hostname: u.hostname,
path: u.pathname + u.search,
port: 443,
headers: {
Authorization: "SSWS " + this.dirConfig.token,
Accept: "application/json",
},
},
(res) => {
let body = "";
res.on("data", (chunk) => {
body += chunk;
});
res.on("end", () => {
if (res.statusCode !== 200) {
resolve(null);
return;
}
const responseJson = JSON.parse(body);
if (res.headers != null) {
const headersMap = new Map<string, string | string[]>();
for (const key in res.headers) {
if (res.headers.hasOwnProperty(key)) {
const val = res.headers[key];
headersMap.set(key.toLowerCase(), val);
} }
}); }
resolve([responseJson, headersMap]);
// Deactivated users have to be queried for separately, only when no filter is provided in the first query return;
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)) resolve([responseJson, null]);
.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]); res.on("error", () => {
return entries; resolve(null);
});
}
);
});
}
private async apiGetMany(endpoint: string, currentData: any[] = []): Promise<any[]> {
const url =
endpoint.indexOf("https://") === 0 ? endpoint : `${this.dirConfig.orgUrl}/api/v1/${endpoint}`;
const response = await this.apiGetCall(url);
if (response == null || response[0] == null || !Array.isArray(response[0])) {
throw new Error("API call failed.");
} }
if (response[0].length === 0) {
private buildUser(user: any) { return currentData;
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;
} }
currentData = currentData.concat(response[0]);
private async getGroups(force: boolean, setFilter: [boolean, Set<string>]): Promise<GroupEntry[]> { if (response[1] == null) {
const entries: GroupEntry[] = []; return currentData;
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;
} }
const linkHeader = response[1].get("link");
private async buildGroup(group: any): Promise<GroupEntry> { if (linkHeader == null || Array.isArray(linkHeader)) {
const entry = new GroupEntry(); return currentData;
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;
} }
let nextLink: string = null;
private buildOktaFilter(baseFilter: string, force: boolean, lastSync: Date) { const linkHeaderParts = linkHeader.split(",");
baseFilter = this.createDirectoryQuery(baseFilter); for (const part of linkHeaderParts) {
baseFilter = baseFilter == null || baseFilter.trim() === '' ? null : baseFilter; if (part.indexOf('; rel="next"') > -1) {
if (force || lastSync == null) { const subParts = part.split(";");
return baseFilter; 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;
} }
if (nextLink == null) {
private encodeUrlParameter(filter: string): string { return currentData;
return filter == null ? '' : encodeURIComponent(filter);
}
private async apiGetCall(url: string): Promise<[any, Map<string, string | string[]>]> {
const u = new URL(url);
return new Promise(resolve => {
https.get({
hostname: u.hostname,
path: u.pathname + u.search,
port: 443,
headers: {
Authorization: 'SSWS ' + this.dirConfig.token,
Accept: 'application/json',
},
}, res => {
let body = '';
res.on('data', chunk => {
body += chunk;
});
res.on('end', () => {
if (res.statusCode !== 200) {
resolve(null);
return;
}
const responseJson = JSON.parse(body);
if (res.headers != null) {
const headersMap = new Map<string, string | string[]>();
for (const key in res.headers) {
if (res.headers.hasOwnProperty(key)) {
const val = res.headers[key];
headersMap.set(key.toLowerCase(), val);
}
}
resolve([responseJson, headersMap]);
return;
}
resolve([responseJson, null]);
});
res.on('error', () => {
resolve(null);
});
});
});
}
private async apiGetMany(endpoint: string, currentData: any[] = []): Promise<any[]> {
const url = endpoint.indexOf('https://') === 0 ? endpoint : `${this.dirConfig.orgUrl}/api/v1/${endpoint}`;
const response = await this.apiGetCall(url);
if (response == null || response[0] == null || !Array.isArray(response[0])) {
throw new Error('API call failed.');
}
if (response[0].length === 0) {
return currentData;
}
currentData = currentData.concat(response[0]);
if (response[1] == null) {
return currentData;
}
const linkHeader = response[1].get('link');
if (linkHeader == null || Array.isArray(linkHeader)) {
return currentData;
}
let nextLink: string = null;
const linkHeaderParts = linkHeader.split(',');
for (const part of linkHeaderParts) {
if (part.indexOf('; rel="next"') > -1) {
const subParts = part.split(';');
if (subParts.length > 0 && subParts[0].indexOf('https://') > -1) {
nextLink = subParts[0].replace('>', '').replace('<', '').trim();
break;
}
}
}
if (nextLink == null) {
return currentData;
}
return this.apiGetMany(nextLink, currentData);
} }
return this.apiGetMany(nextLink, currentData);
}
} }

View File

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

View File

@@ -1,220 +1,272 @@
import { DirectoryType } from '../enums/directoryType'; import { DirectoryType } from "../enums/directoryType";
import { GroupEntry } from '../models/groupEntry'; import { GroupEntry } from "../models/groupEntry";
import { SyncConfiguration } from '../models/syncConfiguration'; import { SyncConfiguration } from "../models/syncConfiguration";
import { UserEntry } from '../models/userEntry'; 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 { ApiService } from "jslib-common/abstractions/api.service";
import { CryptoFunctionService } from 'jslib-common/abstractions/cryptoFunction.service'; import { CryptoFunctionService } from "jslib-common/abstractions/cryptoFunction.service";
import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from 'jslib-common/abstractions/log.service'; import { LogService } from "jslib-common/abstractions/log.service";
import { MessagingService } from 'jslib-common/abstractions/messaging.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 { AzureDirectoryService } from "./azure-directory.service";
import { ConfigurationService } from './configuration.service'; import { ConfigurationService } from "./configuration.service";
import { IDirectoryService } from './directory.service'; import { IDirectoryService } from "./directory.service";
import { GSuiteDirectoryService } from './gsuite-directory.service'; import { GSuiteDirectoryService } from "./gsuite-directory.service";
import { LdapDirectoryService } from './ldap-directory.service'; import { LdapDirectoryService } from "./ldap-directory.service";
import { OktaDirectoryService } from './okta-directory.service'; import { OktaDirectoryService } from "./okta-directory.service";
import { OneLoginDirectoryService } from './onelogin-directory.service'; import { OneLoginDirectoryService } from "./onelogin-directory.service";
export class SyncService { export class SyncService {
private dirType: DirectoryType; private dirType: DirectoryType;
constructor(private configurationService: ConfigurationService, private logService: LogService, constructor(
private cryptoFunctionService: CryptoFunctionService, private apiService: ApiService, private configurationService: ConfigurationService,
private messagingService: MessagingService, private i18nService: I18nService, private logService: LogService,
private environmentService: EnvironmentService) { } private cryptoFunctionService: CryptoFunctionService,
private apiService: ApiService,
private messagingService: MessagingService,
private i18nService: I18nService,
private environmentService: EnvironmentService
) {}
async sync(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> { async sync(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> {
this.dirType = await this.configurationService.getDirectoryType(); this.dirType = await this.configurationService.getDirectoryType();
if (this.dirType == null) { if (this.dirType == null) {
throw new Error('No directory configured.'); 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;
}
} }
private removeDuplicateUsers(users: UserEntry[]) { const directoryService = this.getDirectoryService();
const uniqueUsers = new Array<UserEntry>(); if (directoryService == null) {
const processedActiveUsers = new Map<string, string>(); throw new Error("Cannot load directory service.");
const processedDeletedUsers = new Map<string, string>();
const duplicateEmails = new Array<string>();
// 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;
} }
private filterUnsupportedUsers(users: UserEntry[]): UserEntry[] { const syncConfig = await this.configurationService.getSync();
return users == null ? null : users.filter(u => u.email?.length <= 256); 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<UserEntry>();
const processedActiveUsers = new Map<string, string>();
const processedDeletedUsers = new Map<string, string>();
const duplicateEmails = new Array<string>();
// 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<string> { return uniqueUsers;
let allUsers = new Set<string>(); }
if (allGroups == null) {
return allUsers;
}
for (const group of levelGroups) {
const childGroups = allGroups.filter(g => group.groupMemberReferenceIds.has(g.referenceId));
const childUsers = this.flattenUsersToGroups(childGroups, allGroups);
childUsers.forEach(id => group.userMemberExternalIds.add(id));
allUsers = new Set([...allUsers, ...group.userMemberExternalIds]);
}
return allUsers;
}
private getDirectoryService(): IDirectoryService { private filterUnsupportedUsers(users: UserEntry[]): UserEntry[] {
switch (this.dirType) { return users == null ? null : users.filter((u) => u.email?.length <= 256);
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, private flattenUsersToGroups(levelGroups: GroupEntry[], allGroups: GroupEntry[]): Set<string> {
largeImport: boolean = false) { let allUsers = new Set<string>();
return new OrganizationImportRequest({ if (allGroups == null) {
groups: (groups ?? []).map(g => { return allUsers;
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,
});
} }
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) { private getDirectoryService(): IDirectoryService {
if (syncConfig.groups) { switch (this.dirType) {
await this.configurationService.saveLastGroupSyncDate(time); case DirectoryType.GSuite:
} return new GSuiteDirectoryService(
if (syncConfig.users) { this.configurationService,
await this.configurationService.saveLastUserSyncDate(time); 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);
}
}
} }

View File

@@ -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 { Entry } from "./models/entry";
import { LdapConfiguration } from './models/ldapConfiguration'; import { LdapConfiguration } from "./models/ldapConfiguration";
import { SimResult } from './models/simResult'; import { SimResult } from "./models/simResult";
import { SyncConfiguration } from './models/syncConfiguration'; import { SyncConfiguration } from "./models/syncConfiguration";
import { UserEntry } from './models/userEntry'; import { UserEntry } from "./models/userEntry";
export class ConnectorUtils { export class ConnectorUtils {
static async simulate(syncService: SyncService, i18nService: I18nService, sinceLast: boolean): Promise<SimResult> { static async simulate(
return new Promise(async (resolve, reject) => { syncService: SyncService,
const simResult = new SimResult(); i18nService: I18nService,
try { sinceLast: boolean
const result = await syncService.sync(!sinceLast, true); ): Promise<SimResult> {
if (result[0] != null) { return new Promise(async (resolve, reject) => {
simResult.groups = result[0]; const simResult = new SimResult();
} try {
if (result[1] != null) { const result = await syncService.sync(!sinceLast, true);
simResult.users = result[1]; if (result[0] != null) {
} simResult.groups = result[0];
} catch (e) { }
simResult.groups = null; if (result[1] != null) {
simResult.users = null; simResult.users = result[1];
reject(e || i18nService.t('syncError')); }
return; } catch (e) {
} simResult.groups = null;
simResult.users = null;
reject(e || i18nService.t("syncError"));
return;
}
const userMap = new Map<string, UserEntry>(); const userMap = new Map<string, UserEntry>();
this.sortEntries(simResult.users, i18nService); this.sortEntries(simResult.users, i18nService);
for (const u of simResult.users) { for (const u of simResult.users) {
userMap.set(u.externalId, u); userMap.set(u.externalId, u);
if (u.deleted) { if (u.deleted) {
simResult.deletedUsers.push(u); simResult.deletedUsers.push(u);
} else if (u.disabled) { } else if (u.disabled) {
simResult.disabledUsers.push(u); simResult.disabledUsers.push(u);
} else { } else {
simResult.enabledUsers.push(u); simResult.enabledUsers.push(u);
} }
} }
this.sortEntries(simResult.groups, i18nService); this.sortEntries(simResult.groups, i18nService);
for (const g of simResult.groups) { for (const g of simResult.groups) {
if (g.userMemberExternalIds == null) { if (g.userMemberExternalIds == null) {
continue; continue;
}
const anyG = (g as any);
anyG.users = [];
for (const uid of g.userMemberExternalIds) {
if (userMap.has(uid)) {
anyG.users.push(userMap.get(uid));
} else {
anyG.users.push({ displayName: uid });
}
}
this.sortEntries(anyG.users, i18nService);
}
resolve(simResult);
});
}
static adjustConfigForSave(ldap: LdapConfiguration, sync: SyncConfiguration) {
if (ldap.ad) {
sync.creationDateAttribute = 'whenCreated';
sync.revisionDateAttribute = 'whenChanged';
sync.emailPrefixAttribute = 'sAMAccountName';
sync.memberAttribute = 'member';
sync.userObjectClass = 'person';
sync.groupObjectClass = 'group';
sync.userEmailAttribute = 'mail';
sync.groupNameAttribute = 'name';
if (sync.groupPath == null) {
sync.groupPath = 'CN=Users';
}
if (sync.userPath == null) {
sync.userPath = 'CN=Users';
}
} }
if (sync.interval != null) { const anyG = g as any;
if (sync.interval <= 0) { anyG.users = [];
sync.interval = null; for (const uid of g.userMemberExternalIds) {
} else if (sync.interval < 5) { if (userMap.has(uid)) {
sync.interval = 5; 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) { if (sync.interval != null) {
arr.sort((a, b) => { if (sync.interval <= 0) {
return i18nService.collator ? i18nService.collator.compare(a.displayName, b.displayName) : sync.interval = null;
a.displayName.localeCompare(b.displayName); } else if (sync.interval < 5) {
}); sync.interval = 5;
}
} }
}
private static sortEntries(arr: Entry[], i18nService: I18nService) {
arr.sort((a, b) => {
return i18nService.collator
? i18nService.collator.compare(a.displayName, b.displayName)
: a.displayName.localeCompare(b.displayName);
});
}
} }

View File

@@ -12,28 +12,15 @@
"types": [], "types": [],
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"tldjs": [ "tldjs": ["jslib/src/misc/tldjs.noop"],
"jslib/src/misc/tldjs.noop" "jslib-common/*": ["jslib/common/src/*"],
], "jslib-angular/*": ["jslib/angular/src/*"],
"jslib-common/*": [ "jslib-electron/*": ["jslib/electron/src/*"],
"jslib/common/src/*" "jslib-node/*": ["jslib/node/src/*"]
],
"jslib-angular/*": [
"jslib/angular/src/*"
],
"jslib-electron/*": [
"jslib/electron/src/*"
],
"jslib-node/*": [
"jslib/node/src/*"
]
} }
}, },
"angularCompilerOptions": { "angularCompilerOptions": {
"preserveWhitespaces": true "preserveWhitespaces": true
}, },
"include": [ "include": ["src", "src-cli"]
"src",
"src-cli"
]
} }

View File

@@ -1,17 +1,17 @@
{ {
"extends": "tslint:recommended", "extends": "tslint:recommended",
"rules": { "rules": {
"align": [ true, "statements", "members" ], "align": [true, "statements", "members"],
"ban-types": { "ban-types": {
"options": [ "options": [
[ "Object", "Avoid using the `Object` type. Did you mean `object`?" ], ["Object", "Avoid using the `Object` type. Did you mean `object`?"],
[ "Boolean", "Avoid using the `Boolean` type. Did you mean `boolean`?" ], ["Boolean", "Avoid using the `Boolean` type. Did you mean `boolean`?"],
[ "Number", "Avoid using the `Number` type. Did you mean `number`?" ], ["Number", "Avoid using the `Number` type. Did you mean `number`?"],
[ "String", "Avoid using the `String` type. Did you mean `string`?" ], ["String", "Avoid using the `String` type. Did you mean `string`?"],
[ "Symbol", "Avoid using the `Symbol` type. Did you mean `symbol`?" ] ["Symbol", "Avoid using the `Symbol` type. Did you mean `symbol`?"]
] ]
}, },
"member-access": [ true, "no-public" ], "member-access": [true, "no-public"],
"member-ordering": [ "member-ordering": [
true, true,
{ {
@@ -34,11 +34,10 @@
] ]
} }
], ],
"no-empty": [ true ], "no-empty": [true],
"object-literal-sort-keys": false, "object-literal-sort-keys": false,
"object-literal-shorthand": [ true, "never" ], "object-literal-shorthand": [true, "never"],
"prefer-for-of": false, "prefer-for-of": false,
"quotemark": [ true, "single" ],
"whitespace": [ "whitespace": [
true, true,
"check-branch", "check-branch",
@@ -51,7 +50,7 @@
], ],
"max-classes-per-file": false, "max-classes-per-file": false,
"ordered-imports": true, "ordered-imports": true,
"arrow-parens": [ true ], "arrow-parens": [true],
"trailing-comma": [ "trailing-comma": [
true, true,
{ {

View File

@@ -1,79 +1,77 @@
const path = require('path'); const path = require("path");
const webpack = require('webpack'); const webpack = require("webpack");
const { CleanWebpackPlugin } = require('clean-webpack-plugin'); const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const CopyWebpackPlugin = require('copy-webpack-plugin'); const CopyWebpackPlugin = require("copy-webpack-plugin");
const nodeExternals = require('webpack-node-externals'); const nodeExternals = require("webpack-node-externals");
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
if (process.env.NODE_ENV == null) { 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 = [ const moduleRules = [
{ {
test: /\.ts$/, test: /\.ts$/,
enforce: 'pre', enforce: "pre",
loader: 'tslint-loader', loader: "tslint-loader",
}, },
{ {
test: /\.ts$/, test: /\.ts$/,
use: 'ts-loader', use: "ts-loader",
exclude: path.resolve(__dirname, 'node_modules'), exclude: path.resolve(__dirname, "node_modules"),
}, },
{ {
test: /\.node$/, test: /\.node$/,
loader: 'node-loader', loader: "node-loader",
}, },
]; ];
const plugins = [ const plugins = [
new CleanWebpackPlugin(), new CleanWebpackPlugin(),
new CopyWebpackPlugin({ new CopyWebpackPlugin({
patterns: [ patterns: [{ from: "./src/locales", to: "locales" }],
{ from: './src/locales', to: 'locales' }, }),
], new webpack.DefinePlugin({
}), "process.env.BWCLI_ENV": JSON.stringify(ENV),
new webpack.DefinePlugin({ }),
'process.env.BWCLI_ENV': JSON.stringify(ENV), new webpack.BannerPlugin({
}), banner: "#!/usr/bin/env node",
new webpack.BannerPlugin({ raw: true,
banner: '#!/usr/bin/env node', }),
raw: true new webpack.IgnorePlugin({
}), resourceRegExp: /^encoding$/,
new webpack.IgnorePlugin({ contextRegExp: /node-fetch/,
resourceRegExp: /^encoding$/, }),
contextRegExp: /node-fetch/,
}),
]; ];
const config = { const config = {
mode: ENV, mode: ENV,
target: 'node', target: "node",
devtool: ENV === 'development' ? 'eval-source-map' : 'source-map', devtool: ENV === "development" ? "eval-source-map" : "source-map",
node: { node: {
__dirname: false, __dirname: false,
__filename: false, __filename: false,
}, },
entry: { entry: {
'bwdc': './src/bwdc.ts', bwdc: "./src/bwdc.ts",
}, },
optimization: { optimization: {
minimize: false, minimize: false,
}, },
resolve: { resolve: {
extensions: ['.ts', '.js', '.json'], extensions: [".ts", ".js", ".json"],
plugins: [new TsconfigPathsPlugin({ configFile: './tsconfig.json' })], plugins: [new TsconfigPathsPlugin({ configFile: "./tsconfig.json" })],
symlinks: false, symlinks: false,
modules: [path.resolve('node_modules')], modules: [path.resolve("node_modules")],
}, },
output: { output: {
filename: '[name].js', filename: "[name].js",
path: path.resolve(__dirname, 'build-cli'), path: path.resolve(__dirname, "build-cli"),
}, },
module: { rules: moduleRules }, module: { rules: moduleRules },
plugins: plugins, plugins: plugins,
externals: [nodeExternals()], externals: [nodeExternals()],
}; };
module.exports = config; module.exports = config;

View File

@@ -1,68 +1,68 @@
const path = require('path'); const path = require("path");
const { merge } = require('webpack-merge'); const { merge } = require("webpack-merge");
const CopyWebpackPlugin = require('copy-webpack-plugin'); const CopyWebpackPlugin = require("copy-webpack-plugin");
const { CleanWebpackPlugin } = require('clean-webpack-plugin'); const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const nodeExternals = require('webpack-node-externals'); const nodeExternals = require("webpack-node-externals");
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
const common = { const common = {
module: { module: {
rules: [ rules: [
{ {
test: /\.ts$/, test: /\.ts$/,
enforce: 'pre', enforce: "pre",
loader: 'tslint-loader', loader: "tslint-loader",
}, },
{ {
test: /\.tsx?$/, test: /\.tsx?$/,
use: 'ts-loader', use: "ts-loader",
exclude: /node_modules\/(?!(@bitwarden)\/).*/, exclude: /node_modules\/(?!(@bitwarden)\/).*/,
}, },
], ],
}, },
plugins: [], plugins: [],
resolve: { resolve: {
extensions: ['.tsx', '.ts', '.js'], extensions: [".tsx", ".ts", ".js"],
plugins: [new TsconfigPathsPlugin({ configFile: './tsconfig.json' })], plugins: [new TsconfigPathsPlugin({ configFile: "./tsconfig.json" })],
}, },
output: { output: {
filename: '[name].js', filename: "[name].js",
path: path.resolve(__dirname, 'build'), path: path.resolve(__dirname, "build"),
}, },
}; };
const main = { const main = {
mode: 'production', mode: "production",
target: 'electron-main', target: "electron-main",
node: { node: {
__dirname: false, __dirname: false,
__filename: false, __filename: false,
}, },
entry: { entry: {
'main': './src/main.ts', main: "./src/main.ts",
}, },
optimization: { optimization: {
minimize: false, minimize: false,
}, },
module: { module: {
rules: [ rules: [
{ {
test: /\.node$/, test: /\.node$/,
loader: 'node-loader', loader: "node-loader",
}, },
],
},
plugins: [
new CleanWebpackPlugin(),
new CopyWebpackPlugin({
patterns: [
'./src/package.json',
{ from: './src/images', to: 'images' },
{ from: './src/locales', to: 'locales' },
],
}),
], ],
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); module.exports = merge(common, main);

View File

@@ -1,127 +1,129 @@
const path = require('path'); const path = require("path");
const webpack = require('webpack'); const webpack = require("webpack");
const { merge } = require('webpack-merge'); const { merge } = require("webpack-merge");
const HtmlWebpackPlugin = require('html-webpack-plugin'); const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const { AngularWebpackPlugin } = require('@ngtools/webpack'); const { AngularWebpackPlugin } = require("@ngtools/webpack");
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
const common = { const common = {
module: { module: {
rules: [ rules: [
{ {
test: /\.ts$/, test: /\.ts$/,
enforce: 'pre', enforce: "pre",
loader: 'tslint-loader', loader: "tslint-loader",
}, },
{ {
test: /(?:\.ngfactory\.js|\.ngstyle\.js|\.ts)$/, test: /(?:\.ngfactory\.js|\.ngstyle\.js|\.ts)$/,
loader: '@ngtools/webpack', loader: "@ngtools/webpack",
}, },
{ {
test: /\.(jpe?g|png|gif|svg)$/i, test: /\.(jpe?g|png|gif|svg)$/i,
exclude: /.*(fontawesome-webfont)\.svg/, exclude: /.*(fontawesome-webfont)\.svg/,
generator: { generator: {
filename: 'images/[name].[ext]', filename: "images/[name].[ext]",
}, },
type: 'asset/resource', type: "asset/resource",
}, },
], ],
}, },
plugins: [], plugins: [],
resolve: { resolve: {
extensions: ['.tsx', '.ts', '.js', '.json'], extensions: [".tsx", ".ts", ".js", ".json"],
plugins: [new TsconfigPathsPlugin({ configFile: './tsconfig.json' })], plugins: [new TsconfigPathsPlugin({ configFile: "./tsconfig.json" })],
symlinks: false, symlinks: false,
modules: [path.resolve('node_modules')], modules: [path.resolve("node_modules")],
}, },
output: { output: {
filename: '[name].js', filename: "[name].js",
path: path.resolve(__dirname, 'build'), path: path.resolve(__dirname, "build"),
}, },
}; };
const renderer = { const renderer = {
mode: 'production', mode: "production",
devtool: false, devtool: false,
target: 'electron-renderer', target: "electron-renderer",
node: { node: {
__dirname: false, __dirname: false,
}, },
entry: { entry: {
'app/main': './src/app/main.ts', "app/main": "./src/app/main.ts",
}, },
optimization: { optimization: {
minimize: false, minimize: false,
splitChunks: { splitChunks: {
cacheGroups: { cacheGroups: {
commons: { commons: {
test: /[\\/]node_modules[\\/]/, test: /[\\/]node_modules[\\/]/,
name: 'app/vendor', name: "app/vendor",
chunks: (chunk) => { chunks: (chunk) => {
return chunk.name === 'app/main'; return chunk.name === "app/main";
}, },
},
},
}, },
},
}, },
module: { },
rules: [ module: {
{ rules: [
test: /\.(html)$/, {
loader: 'html-loader', test: /\.(html)$/,
}, loader: "html-loader",
{ },
test: /.(ttf|otf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/, {
exclude: /loading.svg/, test: /.(ttf|otf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/,
generator: { exclude: /loading.svg/,
filename: 'fonts/[name].[ext]', generator: {
}, filename: "fonts/[name].[ext]",
type: 'asset/resource', },
}, type: "asset/resource",
{ },
test: /\.scss$/, {
use: [ test: /\.scss$/,
{ use: [
loader: MiniCssExtractPlugin.loader, {
options: { loader: MiniCssExtractPlugin.loader,
publicPath: '../', options: {
}, publicPath: "../",
},
'css-loader',
'sass-loader',
],
},
// Hide System.import warnings. ref: https://github.com/angular/angular/issues/21560
{
test: /[\/\\]@angular[\/\\].+\.js$/,
parser: { system: true },
}, },
},
"css-loader",
"sass-loader",
], ],
}, },
plugins: [ // Hide System.import warnings. ref: https://github.com/angular/angular/issues/21560
new AngularWebpackPlugin({ {
tsConfigPath: 'tsconfig.json', test: /[\/\\]@angular[\/\\].+\.js$/,
entryModule: 'src/app/app.module#AppModule', parser: { system: true },
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',
}),
], ],
},
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); module.exports = merge(common, renderer);