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
- [ ] Bug fix
- [ ] New feature development
- [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc)
@@ -6,27 +7,26 @@
- [ ] Other
## Objective
<!--Describe what the purpose of this PR is. For example: what bug you're fixing or what new feature you're adding-->
## Code 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-->
* **file.ext:** Description of what was changed and why
- **file.ext:** Description of what was changed and why
## Screenshots
<!--Required for any UI changes. Delete if not applicable-->
## Testing requirements
<!--What functionality requires testing by QA? This includes testing new behavior and regression testing-->
## Before you submit
- [ ] I have checked for **linting** errors (`npm run lint`) (required)
- [ ] I have added **unit tests** where it makes sense to do so (encouraged but not required)
- [ ] This change requires a **documentation update** (notify the documentation team)

66
.vscode/launch.json vendored
View File

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

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

View File

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

View File

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

View File

@@ -1,47 +1,60 @@
<div class="container-fluid">
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<div class="row justify-content-center">
<div class="col-md-8 col-lg-6">
<p class="text-center font-weight-bold">{{'welcome' | i18n}}</p>
<p class="text-center">{{'logInDesc' | i18n}}</p>
<div class="card">
<h5 class="card-header">{{'logIn' | i18n}}</h5>
<div class="card-body">
<div class="form-group">
<label for="client_id">{{'clientId' | i18n}}</label>
<input id="client_id" name="ClientId" [(ngModel)]="clientId"
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>
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<div class="row justify-content-center">
<div class="col-md-8 col-lg-6">
<p class="text-center font-weight-bold">{{ "welcome" | i18n }}</p>
<p class="text-center">{{ "logInDesc" | i18n }}</p>
<div class="card">
<h5 class="card-header">{{ "logIn" | i18n }}</h5>
<div class="card-body">
<div class="form-group">
<label for="client_id">{{ "clientId" | i18n }}</label>
<input id="client_id" name="ClientId" [(ngModel)]="clientId" 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>
</form>
</div>
</div>
</form>
</div>
<ng-template #environment></ng-template>

View File

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

View File

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

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

View File

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

View File

@@ -1,155 +1,166 @@
import {
Component,
NgZone,
OnInit,
SecurityContext,
ViewChild,
ViewContainerRef,
} from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
import { Router } from '@angular/router';
import {
IndividualConfig,
ToastrService,
} from 'ngx-toastr';
Component,
NgZone,
OnInit,
SecurityContext,
ViewChild,
ViewContainerRef,
} from "@angular/core";
import { DomSanitizer } from "@angular/platform-browser";
import { Router } from "@angular/router";
import { IndividualConfig, ToastrService } from "ngx-toastr";
import { AuthService } from 'jslib-common/abstractions/auth.service';
import { BroadcasterService } from 'jslib-common/abstractions/broadcaster.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { MessagingService } from 'jslib-common/abstractions/messaging.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { StateService } from 'jslib-common/abstractions/state.service';
import { TokenService } from 'jslib-common/abstractions/token.service';
import { UserService } from 'jslib-common/abstractions/user.service';
import { AuthService } from "jslib-common/abstractions/auth.service";
import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { TokenService } from "jslib-common/abstractions/token.service";
import { UserService } from "jslib-common/abstractions/user.service";
import { ConfigurationService } from '../services/configuration.service';
import { SyncService } from '../services/sync.service';
import { ConfigurationService } from "../services/configuration.service";
import { SyncService } from "../services/sync.service";
const BroadcasterSubscriptionId = 'AppComponent';
const BroadcasterSubscriptionId = "AppComponent";
@Component({
selector: 'app-root',
styles: [],
template: `
<ng-template #settings></ng-template>
<router-outlet></router-outlet>`,
selector: "app-root",
styles: [],
template: ` <ng-template #settings></ng-template>
<router-outlet></router-outlet>`,
})
export class AppComponent implements OnInit {
@ViewChild('settings', { read: ViewContainerRef, static: true }) settingsRef: ViewContainerRef;
@ViewChild("settings", { read: ViewContainerRef, static: true }) settingsRef: ViewContainerRef;
constructor(private broadcasterService: BroadcasterService, private userService: UserService,
private tokenService: TokenService,
private authService: AuthService, private router: Router,
private toastrService: ToastrService, private i18nService: I18nService,
private sanitizer: DomSanitizer, private ngZone: NgZone,
private platformUtilsService: PlatformUtilsService, private messagingService: MessagingService,
private configurationService: ConfigurationService, private syncService: SyncService,
private stateService: StateService, private logService: LogService) {
}
constructor(
private broadcasterService: BroadcasterService,
private userService: UserService,
private tokenService: TokenService,
private authService: AuthService,
private router: Router,
private toastrService: ToastrService,
private i18nService: I18nService,
private sanitizer: DomSanitizer,
private ngZone: NgZone,
private platformUtilsService: PlatformUtilsService,
private messagingService: MessagingService,
private configurationService: ConfigurationService,
private syncService: SyncService,
private stateService: StateService,
private logService: LogService
) {}
ngOnInit() {
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
this.ngZone.run(async () => {
switch (message.command) {
case 'syncScheduleStarted':
case 'syncScheduleStopped':
this.stateService.save('syncingDir', message.command === 'syncScheduleStarted');
break;
case 'logout':
this.logOut(!!message.expired);
break;
case 'checkDirSync':
try {
const syncConfig = await this.configurationService.getSync();
if (syncConfig.interval == null || syncConfig.interval < 5) {
return;
}
ngOnInit() {
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
this.ngZone.run(async () => {
switch (message.command) {
case "syncScheduleStarted":
case "syncScheduleStopped":
this.stateService.save("syncingDir", message.command === "syncScheduleStarted");
break;
case "logout":
this.logOut(!!message.expired);
break;
case "checkDirSync":
try {
const syncConfig = await this.configurationService.getSync();
if (syncConfig.interval == null || syncConfig.interval < 5) {
return;
}
const syncInterval = syncConfig.interval * 60000;
const lastGroupSync = await this.configurationService.getLastGroupSyncDate();
const lastUserSync = await this.configurationService.getLastUserSyncDate();
let lastSync: Date = null;
if (lastGroupSync != null && lastUserSync == null) {
lastSync = lastGroupSync;
} else if (lastGroupSync == null && lastUserSync != null) {
lastSync = lastUserSync;
} else if (lastGroupSync != null && lastUserSync != null) {
if (lastGroupSync.getTime() < lastUserSync.getTime()) {
lastSync = lastGroupSync;
} else {
lastSync = lastUserSync;
}
}
let lastSyncAgo = syncInterval + 1;
if (lastSync != null) {
lastSyncAgo = new Date().getTime() - lastSync.getTime();
}
if (lastSyncAgo >= syncInterval) {
await this.syncService.sync(false, false);
}
} catch (e) {
this.logService.error(e);
}
this.messagingService.send('scheduleNextDirSync');
break;
case 'showToast':
this.showToast(message);
break;
case 'ssoCallback':
this.router.navigate(['sso'], { queryParams: { code: message.code, state: message.state } });
break;
default:
const syncInterval = syncConfig.interval * 60000;
const lastGroupSync = await this.configurationService.getLastGroupSyncDate();
const lastUserSync = await this.configurationService.getLastUserSyncDate();
let lastSync: Date = null;
if (lastGroupSync != null && lastUserSync == null) {
lastSync = lastGroupSync;
} else if (lastGroupSync == null && lastUserSync != null) {
lastSync = lastUserSync;
} else if (lastGroupSync != null && lastUserSync != null) {
if (lastGroupSync.getTime() < lastUserSync.getTime()) {
lastSync = lastGroupSync;
} else {
lastSync = lastUserSync;
}
}
let lastSyncAgo = syncInterval + 1;
if (lastSync != null) {
lastSyncAgo = new Date().getTime() - lastSync.getTime();
}
if (lastSyncAgo >= syncInterval) {
await this.syncService.sync(false, false);
}
} catch (e) {
this.logService.error(e);
}
this.messagingService.send("scheduleNextDirSync");
break;
case "showToast":
this.showToast(message);
break;
case "ssoCallback":
this.router.navigate(["sso"], {
queryParams: { code: message.code, state: message.state },
});
});
}
ngOnDestroy() {
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
}
private async logOut(expired: boolean) {
const userId = await this.userService.getUserId();
await this.tokenService.clearToken();
await this.userService.clear();
this.authService.logOut(async () => {
if (expired) {
this.platformUtilsService.showToast('warning', this.i18nService.t('loggedOut'),
this.i18nService.t('loginExpired'));
}
this.router.navigate(['login']);
});
}
private showToast(msg: any) {
let message = '';
const options: Partial<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;
}
break;
default:
}
});
});
}
this.toastrService.show(message, msg.title, options, 'toast-' + msg.type);
ngOnDestroy() {
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
}
private async logOut(expired: boolean) {
const userId = await this.userService.getUserId();
await this.tokenService.clearToken();
await this.userService.clear();
this.authService.logOut(async () => {
if (expired) {
this.platformUtilsService.showToast(
"warning",
this.i18nService.t("loggedOut"),
this.i18nService.t("loginExpired")
);
}
this.router.navigate(["login"]);
});
}
private showToast(msg: any) {
let message = "";
const options: Partial<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 'zone.js/dist/zone';
import "core-js/stable";
import "zone.js/dist/zone";
import { AppRoutingModule } from './app-routing.module';
import { ServicesModule } from './services/services.module';
import { AppRoutingModule } from "./app-routing.module";
import { ServicesModule } from "./services/services.module";
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NgModule } from "@angular/core";
import { FormsModule } from "@angular/forms";
import { BrowserModule } from "@angular/platform-browser";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { AppComponent } from './app.component';
import { AppComponent } from "./app.component";
import { CalloutComponent } from 'jslib-angular/components/callout.component';
import { IconComponent } from 'jslib-angular/components/icon.component';
import { BitwardenToastModule } from 'jslib-angular/components/toastr.component';
import { CalloutComponent } from "jslib-angular/components/callout.component";
import { IconComponent } from "jslib-angular/components/icon.component";
import { BitwardenToastModule } from "jslib-angular/components/toastr.component";
import { ApiKeyComponent } from './accounts/apiKey.component';
import { EnvironmentComponent } from './accounts/environment.component';
import { DashboardComponent } from './tabs/dashboard.component';
import { MoreComponent } from './tabs/more.component';
import { SettingsComponent } from './tabs/settings.component';
import { TabsComponent } from './tabs/tabs.component';
import { ApiKeyComponent } from "./accounts/apiKey.component";
import { EnvironmentComponent } from "./accounts/environment.component";
import { DashboardComponent } from "./tabs/dashboard.component";
import { MoreComponent } from "./tabs/more.component";
import { SettingsComponent } from "./tabs/settings.component";
import { TabsComponent } from "./tabs/tabs.component";
import { A11yTitleDirective } from 'jslib-angular/directives/a11y-title.directive';
import { ApiActionDirective } from 'jslib-angular/directives/api-action.directive';
import { AutofocusDirective } from 'jslib-angular/directives/autofocus.directive';
import { BlurClickDirective } from 'jslib-angular/directives/blur-click.directive';
import { BoxRowDirective } from 'jslib-angular/directives/box-row.directive';
import { FallbackSrcDirective } from 'jslib-angular/directives/fallback-src.directive';
import { StopClickDirective } from 'jslib-angular/directives/stop-click.directive';
import { StopPropDirective } from 'jslib-angular/directives/stop-prop.directive';
import { A11yTitleDirective } from "jslib-angular/directives/a11y-title.directive";
import { ApiActionDirective } from "jslib-angular/directives/api-action.directive";
import { AutofocusDirective } from "jslib-angular/directives/autofocus.directive";
import { BlurClickDirective } from "jslib-angular/directives/blur-click.directive";
import { BoxRowDirective } from "jslib-angular/directives/box-row.directive";
import { FallbackSrcDirective } from "jslib-angular/directives/fallback-src.directive";
import { StopClickDirective } from "jslib-angular/directives/stop-click.directive";
import { StopPropDirective } from "jslib-angular/directives/stop-prop.directive";
import { I18nPipe } from 'jslib-angular/pipes/i18n.pipe';
import { SearchCiphersPipe } from 'jslib-angular/pipes/search-ciphers.pipe';
import { I18nPipe } from "jslib-angular/pipes/i18n.pipe";
import { SearchCiphersPipe } from "jslib-angular/pipes/search-ciphers.pipe";
@NgModule({
imports: [
BrowserModule,
BrowserAnimationsModule,
FormsModule,
AppRoutingModule,
ServicesModule,
BitwardenToastModule.forRoot({
maxOpened: 5,
autoDismiss: true,
closeButton: true,
}),
],
declarations: [
A11yTitleDirective,
ApiActionDirective,
ApiKeyComponent,
AppComponent,
AutofocusDirective,
BlurClickDirective,
BoxRowDirective,
CalloutComponent,
DashboardComponent,
EnvironmentComponent,
FallbackSrcDirective,
I18nPipe,
IconComponent,
MoreComponent,
SearchCiphersPipe,
SettingsComponent,
StopClickDirective,
StopPropDirective,
TabsComponent,
],
providers: [],
bootstrap: [AppComponent],
imports: [
BrowserModule,
BrowserAnimationsModule,
FormsModule,
AppRoutingModule,
ServicesModule,
BitwardenToastModule.forRoot({
maxOpened: 5,
autoDismiss: true,
closeButton: true,
}),
],
declarations: [
A11yTitleDirective,
ApiActionDirective,
ApiKeyComponent,
AppComponent,
AutofocusDirective,
BlurClickDirective,
BoxRowDirective,
CalloutComponent,
DashboardComponent,
EnvironmentComponent,
FallbackSrcDirective,
I18nPipe,
IconComponent,
MoreComponent,
SearchCiphersPipe,
SettingsComponent,
StopClickDirective,
StopPropDirective,
TabsComponent,
],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule { }
export class AppModule {}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,99 +1,110 @@
<div class="card mb-3">
<h3 class="card-header">{{'sync' | i18n}}</h3>
<div class="card-body">
<p>
{{'lastGroupSync' | i18n}}:
<span *ngIf="!lastGroupSync">-</span>
{{lastGroupSync | date:'medium'}}
<br /> {{'lastUserSync' | i18n}}:
<span *ngIf="!lastUserSync">-</span>
{{lastUserSync | date:'medium'}}
</p>
<p>
{{'syncStatus' | i18n}}:
<strong *ngIf="syncRunning" class="text-success">{{'running' | i18n}}</strong>
<strong *ngIf="!syncRunning" class="text-danger">{{'stopped' | i18n}}</strong>
</p>
<form #startForm [appApiAction]="startPromise" class="d-inline">
<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-spinner fa-fw fa-spin" [hidden]="!startForm.loading"></i>
{{'startSync' | i18n}}
</button>
</form>
<button (click)="stop()" class="btn btn-primary">
<i class="fa fa-stop fa-fw"></i>
{{'stopSync' | i18n}}
</button>
<form #syncForm [appApiAction]="syncPromise" class="d-inline">
<button (click)="sync()" class="btn btn-primary"
[disabled]="syncForm.loading">
<i class="fa fa-refresh fa-fw" [ngClass]="{'fa-spin': syncForm.loading}"></i>
{{'syncNow' | i18n}}
</button>
</form>
</div>
<h3 class="card-header">{{ "sync" | i18n }}</h3>
<div class="card-body">
<p>
{{ "lastGroupSync" | i18n }}:
<span *ngIf="!lastGroupSync">-</span>
{{ lastGroupSync | date: "medium" }}
<br />
{{ "lastUserSync" | i18n }}:
<span *ngIf="!lastUserSync">-</span>
{{ lastUserSync | date: "medium" }}
</p>
<p>
{{ "syncStatus" | i18n }}:
<strong *ngIf="syncRunning" class="text-success">{{ "running" | i18n }}</strong>
<strong *ngIf="!syncRunning" class="text-danger">{{ "stopped" | i18n }}</strong>
</p>
<form #startForm [appApiAction]="startPromise" class="d-inline">
<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-spinner fa-fw fa-spin" [hidden]="!startForm.loading"></i>
{{ "startSync" | i18n }}
</button>
</form>
<button (click)="stop()" class="btn btn-primary">
<i class="fa fa-stop fa-fw"></i>
{{ "stopSync" | i18n }}
</button>
<form #syncForm [appApiAction]="syncPromise" class="d-inline">
<button (click)="sync()" class="btn btn-primary" [disabled]="syncForm.loading">
<i class="fa fa-refresh fa-fw" [ngClass]="{ 'fa-spin': syncForm.loading }"></i>
{{ "syncNow" | i18n }}
</button>
</form>
</div>
</div>
<div class="card">
<h3 class="card-header">{{'testing' | i18n}}</h3>
<div class="card-body">
<p>{{'testingDesc' | i18n}}</p>
<form #simForm [appApiAction]="simPromise" class="d-inline">
<button (click)="simulate()" class="btn btn-primary"
[disabled]="simForm.loading">
<i class="fa fa-spinner fa-fw fa-spin" [hidden]="!simForm.loading"></i>
<i class="fa fa-bug fa-fw" [hidden]="simForm.loading"></i>
{{'testNow' | i18n}}
</button>
</form>
<div class="form-check mt-2">
<input class="form-check-input" type="checkbox" id="simSinceLast" [(ngModel)]="simSinceLast">
<label class="form-check-label" for="simSinceLast">{{'testLastSync' | i18n}}</label>
</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>
<h3 class="card-header">{{ "testing" | i18n }}</h3>
<div class="card-body">
<p>{{ "testingDesc" | i18n }}</p>
<form #simForm [appApiAction]="simPromise" class="d-inline">
<button (click)="simulate()" class="btn btn-primary" [disabled]="simForm.loading">
<i class="fa fa-spinner fa-fw fa-spin" [hidden]="!simForm.loading"></i>
<i class="fa fa-bug fa-fw" [hidden]="simForm.loading"></i>
{{ "testNow" | i18n }}
</button>
</form>
<div class="form-check mt-2">
<input
class="form-check-input"
type="checkbox"
id="simSinceLast"
[(ngModel)]="simSinceLast"
/>
<label class="form-check-label" for="simSinceLast">{{ "testLastSync" | i18n }}</label>
</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>

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

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

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 { I18nService } from 'jslib-common/abstractions/i18n.service';
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { ConfigurationService } from '../services/configuration.service';
import { ConfigurationService } from "../services/configuration.service";
import { DirectoryType } from '../enums/directoryType';
import { DirectoryType } from "../enums/directoryType";
import { Response } from 'jslib-node/cli/models/response';
import { MessageResponse } from 'jslib-node/cli/models/response/messageResponse';
import { Response } from "jslib-node/cli/models/response";
import { MessageResponse } from "jslib-node/cli/models/response/messageResponse";
import { AzureConfiguration } from '../models/azureConfiguration';
import { GSuiteConfiguration } from '../models/gsuiteConfiguration';
import { LdapConfiguration } from '../models/ldapConfiguration';
import { OktaConfiguration } from '../models/oktaConfiguration';
import { OneLoginConfiguration } from '../models/oneLoginConfiguration';
import { SyncConfiguration } from '../models/syncConfiguration';
import { AzureConfiguration } from "../models/azureConfiguration";
import { GSuiteConfiguration } from "../models/gsuiteConfiguration";
import { LdapConfiguration } from "../models/ldapConfiguration";
import { OktaConfiguration } from "../models/oktaConfiguration";
import { OneLoginConfiguration } from "../models/oneLoginConfiguration";
import { SyncConfiguration } from "../models/syncConfiguration";
import { ConnectorUtils } from '../utils';
import { ConnectorUtils } from "../utils";
import { NodeUtils } from 'jslib-common/misc/nodeUtils';
import { NodeUtils } from "jslib-common/misc/nodeUtils";
export class ConfigCommand {
private directory: DirectoryType;
private ldap = new LdapConfiguration();
private gsuite = new GSuiteConfiguration();
private azure = new AzureConfiguration();
private okta = new OktaConfiguration();
private oneLogin = new OneLoginConfiguration();
private sync = new SyncConfiguration();
private directory: DirectoryType;
private ldap = new LdapConfiguration();
private gsuite = new GSuiteConfiguration();
private azure = new AzureConfiguration();
private okta = new OktaConfiguration();
private oneLogin = new OneLoginConfiguration();
private sync = new SyncConfiguration();
constructor(private environmentService: EnvironmentService, private i18nService: I18nService,
private configurationService: ConfigurationService) { }
constructor(
private environmentService: EnvironmentService,
private i18nService: I18nService,
private configurationService: ConfigurationService
) {}
async run(setting: string, value: string, options: program.OptionValues): Promise<Response> {
setting = setting.toLowerCase();
if (value == null || value === '') {
if (options.secretfile) {
value = await NodeUtils.readFirstLine(options.secretfile);
} else if (options.secretenv && process.env[options.secretenv]) {
value = process.env[options.secretenv];
}
}
try {
switch (setting) {
case 'server':
await this.setServer(value);
break;
case 'directory':
await this.setDirectory(value);
break;
case 'ldap.password':
await this.setLdapPassword(value);
break;
case 'gsuite.key':
await this.setGSuiteKey(value);
break;
case 'azure.key':
await this.setAzureKey(value);
break;
case 'okta.token':
await this.setOktaToken(value);
break;
case 'onelogin.secret':
await this.setOneLoginSecret(value);
break;
default:
return Response.badRequest('Unknown setting.');
}
} catch (e) {
return Response.error(e);
}
const res = new MessageResponse(this.i18nService.t('savedSetting', setting), null);
return Response.success(res);
async run(setting: string, value: string, options: program.OptionValues): Promise<Response> {
setting = setting.toLowerCase();
if (value == null || value === "") {
if (options.secretfile) {
value = await NodeUtils.readFirstLine(options.secretfile);
} else if (options.secretenv && process.env[options.secretenv]) {
value = process.env[options.secretenv];
}
}
private async setServer(url: string) {
url = (url === 'null' || url === 'bitwarden.com' || url === 'https://bitwarden.com' ? null : url);
await this.environmentService.setUrls({
base: url,
});
try {
switch (setting) {
case "server":
await this.setServer(value);
break;
case "directory":
await this.setDirectory(value);
break;
case "ldap.password":
await this.setLdapPassword(value);
break;
case "gsuite.key":
await this.setGSuiteKey(value);
break;
case "azure.key":
await this.setAzureKey(value);
break;
case "okta.token":
await this.setOktaToken(value);
break;
case "onelogin.secret":
await this.setOneLoginSecret(value);
break;
default:
return Response.badRequest("Unknown setting.");
}
} catch (e) {
return Response.error(e);
}
const res = new MessageResponse(this.i18nService.t("savedSetting", setting), null);
return Response.success(res);
}
private async setDirectory(type: string) {
const dir = parseInt(type, null);
if (dir < DirectoryType.Ldap || dir > DirectoryType.OneLogin) {
throw new Error('Invalid directory type value.');
}
await this.loadConfig();
this.directory = dir;
await this.saveConfig();
}
private async setServer(url: string) {
url = url === "null" || url === "bitwarden.com" || url === "https://bitwarden.com" ? null : url;
await this.environmentService.setUrls({
base: url,
});
}
private async setLdapPassword(password: string) {
await this.loadConfig();
this.ldap.password = password;
await this.saveConfig();
private async setDirectory(type: string) {
const dir = parseInt(type, null);
if (dir < DirectoryType.Ldap || dir > DirectoryType.OneLogin) {
throw new Error("Invalid directory type value.");
}
await this.loadConfig();
this.directory = dir;
await this.saveConfig();
}
private async setGSuiteKey(key: string) {
await this.loadConfig();
this.gsuite.privateKey = key != null ? key.trimLeft() : null;
await this.saveConfig();
}
private async setLdapPassword(password: string) {
await this.loadConfig();
this.ldap.password = password;
await this.saveConfig();
}
private async setAzureKey(key: string) {
await this.loadConfig();
this.azure.key = key;
await this.saveConfig();
}
private async setGSuiteKey(key: string) {
await this.loadConfig();
this.gsuite.privateKey = key != null ? key.trimLeft() : null;
await this.saveConfig();
}
private async setOktaToken(token: string) {
await this.loadConfig();
this.okta.token = token;
await this.saveConfig();
}
private async setAzureKey(key: string) {
await this.loadConfig();
this.azure.key = key;
await this.saveConfig();
}
private async setOneLoginSecret(secret: string) {
await this.loadConfig();
this.oneLogin.clientSecret = secret;
await this.saveConfig();
}
private async setOktaToken(token: string) {
await this.loadConfig();
this.okta.token = token;
await this.saveConfig();
}
private async loadConfig() {
this.directory = await this.configurationService.getDirectoryType();
this.ldap = (await this.configurationService.getDirectory<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;
}
private async setOneLoginSecret(secret: string) {
await this.loadConfig();
this.oneLogin.clientSecret = secret;
await this.saveConfig();
}
private async saveConfig() {
ConnectorUtils.adjustConfigForSave(this.ldap, this.sync);
await this.configurationService.saveDirectoryType(this.directory);
await this.configurationService.saveDirectory(DirectoryType.Ldap, this.ldap);
await this.configurationService.saveDirectory(DirectoryType.GSuite, this.gsuite);
await this.configurationService.saveDirectory(DirectoryType.AzureActiveDirectory, this.azure);
await this.configurationService.saveDirectory(DirectoryType.Okta, this.okta);
await this.configurationService.saveDirectory(DirectoryType.OneLogin, this.oneLogin);
await this.configurationService.saveSync(this.sync);
}
private async loadConfig() {
this.directory = await this.configurationService.getDirectoryType();
this.ldap =
(await this.configurationService.getDirectory<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;
}
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 { StringResponse } from 'jslib-node/cli/models/response/stringResponse';
import { Response } from "jslib-node/cli/models/response";
import { StringResponse } from "jslib-node/cli/models/response/stringResponse";
export class LastSyncCommand {
constructor(private configurationService: ConfigurationService) { }
constructor(private configurationService: ConfigurationService) {}
async run(object: string): Promise<Response> {
try {
switch (object.toLowerCase()) {
case 'groups':
const groupsDate = await this.configurationService.getLastGroupSyncDate();
const groupsRes = new StringResponse(groupsDate == null ? null : groupsDate.toISOString());
return Response.success(groupsRes);
case 'users':
const usersDate = await this.configurationService.getLastUserSyncDate();
const usersRes = new StringResponse(usersDate == null ? null : usersDate.toISOString());
return Response.success(usersRes);
default:
return Response.badRequest('Unknown object.');
}
} catch (e) {
return Response.error(e);
}
async run(object: string): Promise<Response> {
try {
switch (object.toLowerCase()) {
case "groups":
const groupsDate = await this.configurationService.getLastGroupSyncDate();
const groupsRes = new StringResponse(
groupsDate == null ? null : groupsDate.toISOString()
);
return Response.success(groupsRes);
case "users":
const usersDate = await this.configurationService.getLastUserSyncDate();
const usersRes = new StringResponse(usersDate == null ? null : usersDate.toISOString());
return Response.success(usersRes);
default:
return Response.badRequest("Unknown object.");
}
} catch (e) {
return Response.error(e);
}
}
}

View File

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

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

View File

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

2
src/global.d.ts vendored
View File

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

View File

@@ -1,16 +1,19 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" 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">
<head>
<meta charset="UTF-8" />
<meta
http-equiv="Content-Security-Policy"
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>
<base href="">
</head>
<body>
<base href="" />
</head>
<body>
<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>
</body>
</body>
</html>

View File

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

View File

@@ -1,68 +1,69 @@
import {
Menu,
MenuItem,
MenuItemConstructorOptions,
} from 'electron';
import { Menu, MenuItem, MenuItemConstructorOptions } from "electron";
import { Main } from '../main';
import { Main } from "../main";
import { BaseMenu } from 'jslib-electron/baseMenu';
import { BaseMenu } from "jslib-electron/baseMenu";
export class MenuMain extends BaseMenu {
menu: Menu;
menu: Menu;
constructor(private main: Main) {
super(main.i18nService, main.windowMain);
constructor(private main: Main) {
super(main.i18nService, main.windowMain);
}
init() {
this.initProperties();
this.initContextMenu();
this.initApplicationMenu();
}
private initApplicationMenu() {
const template: MenuItemConstructorOptions[] = [
this.editMenuItemOptions,
{
label: this.i18nService.t("view"),
submenu: this.viewSubMenuItemOptions,
},
this.windowMenuItemOptions,
];
if (process.platform === "darwin") {
const firstMenuPart: MenuItemConstructorOptions[] = [
{
label: this.i18nService.t("aboutBitwarden"),
role: "about",
},
];
template.unshift({
label: this.main.i18nService.t("bitwardenDirectoryConnector"),
submenu: firstMenuPart.concat(this.macAppMenuItemOptions),
});
// Window menu
template[template.length - 1].submenu = this.macWindowSubmenuOptions;
}
init() {
this.initProperties();
this.initContextMenu();
this.initApplicationMenu();
}
(template[template.length - 1].submenu as MenuItemConstructorOptions[]).splice(
1,
0,
{
label: this.main.i18nService.t(
process.platform === "darwin" ? "hideToMenuBar" : "hideToTray"
),
click: () => this.main.messagingService.send("hideToTray"),
accelerator: "CmdOrCtrl+Shift+M",
},
{
type: "checkbox",
label: this.main.i18nService.t("alwaysOnTop"),
checked: this.windowMain.win.isAlwaysOnTop(),
click: () => this.main.windowMain.toggleAlwaysOnTop(),
accelerator: "CmdOrCtrl+Shift+T",
}
);
private initApplicationMenu() {
const template: MenuItemConstructorOptions[] = [
this.editMenuItemOptions,
{
label: this.i18nService.t('view'),
submenu: this.viewSubMenuItemOptions,
},
this.windowMenuItemOptions,
];
if (process.platform === 'darwin') {
const firstMenuPart: MenuItemConstructorOptions[] = [
{
label: this.i18nService.t('aboutBitwarden'),
role: 'about',
},
];
template.unshift({
label: this.main.i18nService.t('bitwardenDirectoryConnector'),
submenu: firstMenuPart.concat(this.macAppMenuItemOptions),
});
// Window menu
template[template.length - 1].submenu = this.macWindowSubmenuOptions;
}
(template[template.length - 1].submenu as MenuItemConstructorOptions[]).splice(1, 0,
{
label: this.main.i18nService.t(process.platform === 'darwin' ? 'hideToMenuBar' : 'hideToTray'),
click: () => this.main.messagingService.send('hideToTray'),
accelerator: 'CmdOrCtrl+Shift+M',
},
{
type: 'checkbox',
label: this.main.i18nService.t('alwaysOnTop'),
checked: this.windowMain.win.isAlwaysOnTop(),
click: () => this.main.windowMain.toggleAlwaysOnTop(),
accelerator: 'CmdOrCtrl+Shift+T',
});
this.menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(this.menu);
}
this.menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(this.menu);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,22 +1,25 @@
import { GroupResponse } from './groupResponse';
import { UserResponse } from './userResponse';
import { GroupResponse } from "./groupResponse";
import { UserResponse } from "./userResponse";
import { SimResult } from '../simResult';
import { SimResult } from "../simResult";
import { BaseResponse } from 'jslib-node/cli/models/response/baseResponse';
import { BaseResponse } from "jslib-node/cli/models/response/baseResponse";
export class TestResponse implements BaseResponse {
object: string;
groups: GroupResponse[] = [];
enabledUsers: UserResponse[] = [];
disabledUsers: UserResponse[] = [];
deletedUsers: UserResponse[] = [];
object: string;
groups: GroupResponse[] = [];
enabledUsers: UserResponse[] = [];
disabledUsers: UserResponse[] = [];
deletedUsers: UserResponse[] = [];
constructor(result: SimResult) {
this.object = 'test';
this.groups = result.groups != null ? result.groups.map(g => new GroupResponse(g)) : [];
this.enabledUsers = result.enabledUsers != null ? result.enabledUsers.map(u => new UserResponse(u)) : [];
this.disabledUsers = result.disabledUsers != null ? result.disabledUsers.map(u => new UserResponse(u)) : [];
this.deletedUsers = result.deletedUsers != null ? result.deletedUsers.map(u => new UserResponse(u)) : [];
}
constructor(result: SimResult) {
this.object = "test";
this.groups = result.groups != null ? result.groups.map((g) => new GroupResponse(g)) : [];
this.enabledUsers =
result.enabledUsers != null ? result.enabledUsers.map((u) => new UserResponse(u)) : [];
this.disabledUsers =
result.disabledUsers != null ? result.disabledUsers.map((u) => new UserResponse(u)) : [];
this.deletedUsers =
result.deletedUsers != null ? result.deletedUsers.map((u) => new UserResponse(u)) : [];
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,302 +1,330 @@
import * as chalk from 'chalk';
import * as program from 'commander';
import * as path from 'path';
import * as chalk from "chalk";
import * as program from "commander";
import * as path from "path";
import { Main } from './bwdc';
import { Main } from "./bwdc";
import { ClearCacheCommand } from './commands/clearCache.command';
import { ConfigCommand } from './commands/config.command';
import { LastSyncCommand } from './commands/lastSync.command';
import { SyncCommand } from './commands/sync.command';
import { TestCommand } from './commands/test.command';
import { ClearCacheCommand } from "./commands/clearCache.command";
import { ConfigCommand } from "./commands/config.command";
import { LastSyncCommand } from "./commands/lastSync.command";
import { SyncCommand } from "./commands/sync.command";
import { TestCommand } from "./commands/test.command";
import { LoginCommand } from 'jslib-node/cli/commands/login.command';
import { LogoutCommand } from 'jslib-node/cli/commands/logout.command';
import { UpdateCommand } from 'jslib-node/cli/commands/update.command';
import { LoginCommand } from "jslib-node/cli/commands/login.command";
import { LogoutCommand } from "jslib-node/cli/commands/logout.command";
import { UpdateCommand } from "jslib-node/cli/commands/update.command";
import { BaseProgram } from 'jslib-node/cli/baseProgram';
import { BaseProgram } from "jslib-node/cli/baseProgram";
import { ApiKeyService } from 'jslib-common/abstractions/apiKey.service';
import { Response } from 'jslib-node/cli/models/response';
import { StringResponse } from 'jslib-node/cli/models/response/stringResponse';
import { ApiKeyService } from "jslib-common/abstractions/apiKey.service";
import { Response } from "jslib-node/cli/models/response";
import { StringResponse } from "jslib-node/cli/models/response/stringResponse";
import { Utils } from 'jslib-common/misc/utils';
import { Utils } from "jslib-common/misc/utils";
const writeLn = (s: string, finalLine: boolean = false, error: boolean = false) => {
const stream = error ? process.stderr : process.stdout;
if (finalLine && process.platform === 'win32') {
stream.write(s);
} else {
stream.write(s + '\n');
}
const stream = error ? process.stderr : process.stdout;
if (finalLine && process.platform === "win32") {
stream.write(s);
} else {
stream.write(s + "\n");
}
};
export class Program extends BaseProgram {
private apiKeyService: ApiKeyService;
private apiKeyService: ApiKeyService;
constructor(private main: Main) {
super(main.userService, writeLn);
this.apiKeyService = main.apiKeyService;
}
constructor(private main: Main) {
super(main.userService, writeLn);
this.apiKeyService = main.apiKeyService;
}
async run() {
program
.option('--pretty', 'Format output. JSON is tabbed with two spaces.')
.option('--raw', 'Return raw output instead of a descriptive message.')
.option('--response', 'Return a JSON formatted version of response output.')
.option('--cleanexit', 'Exit with a success exit code (0) unless an error is thrown.')
.option('--quiet', 'Don\'t return anything to stdout.')
.option('--nointeraction', 'Do not prompt for interactive user input.')
.version(await this.main.platformUtilsService.getApplicationVersion(), '-v, --version');
async run() {
program
.option("--pretty", "Format output. JSON is tabbed with two spaces.")
.option("--raw", "Return raw output instead of a descriptive message.")
.option("--response", "Return a JSON formatted version of response output.")
.option("--cleanexit", "Exit with a success exit code (0) unless an error is thrown.")
.option("--quiet", "Don't return anything to stdout.")
.option("--nointeraction", "Do not prompt for interactive user input.")
.version(await this.main.platformUtilsService.getApplicationVersion(), "-v, --version");
program.on('option:pretty', () => {
process.env.BW_PRETTY = 'true';
});
program.on("option:pretty", () => {
process.env.BW_PRETTY = "true";
});
program.on('option:raw', () => {
process.env.BW_RAW = 'true';
});
program.on("option:raw", () => {
process.env.BW_RAW = "true";
});
program.on('option:quiet', () => {
process.env.BW_QUIET = 'true';
});
program.on("option:quiet", () => {
process.env.BW_QUIET = "true";
});
program.on('option:response', () => {
process.env.BW_RESPONSE = 'true';
});
program.on("option:response", () => {
process.env.BW_RESPONSE = "true";
});
program.on('option:cleanexit', () => {
process.env.BW_CLEANEXIT = 'true';
});
program.on("option:cleanexit", () => {
process.env.BW_CLEANEXIT = "true";
});
program.on('option:nointeraction', () => {
process.env.BW_NOINTERACTION = 'true';
});
program.on("option:nointeraction", () => {
process.env.BW_NOINTERACTION = "true";
});
program.on('command:*', () => {
writeLn(chalk.redBright('Invalid command: ' + program.args.join(' ')), false, true);
writeLn('See --help for a list of available commands.', true, true);
process.exitCode = 1;
});
program.on("command:*", () => {
writeLn(chalk.redBright("Invalid command: " + program.args.join(" ")), false, true);
writeLn("See --help for a list of available commands.", true, true);
process.exitCode = 1;
});
program.on('--help', () => {
writeLn('\n Examples:');
writeLn('');
writeLn(' bwdc login');
writeLn(' bwdc test');
writeLn(' bwdc sync');
writeLn(' bwdc last-sync');
writeLn(' bwdc config server https://bw.company.com');
writeLn(' bwdc update');
writeLn('', true);
});
program.on("--help", () => {
writeLn("\n Examples:");
writeLn("");
writeLn(" bwdc login");
writeLn(" bwdc test");
writeLn(" bwdc sync");
writeLn(" bwdc last-sync");
writeLn(" bwdc config server https://bw.company.com");
writeLn(" bwdc update");
writeLn("", true);
});
program
.command('login [clientId] [clientSecret]')
.description('Log into an organization account.', {
clientId: 'Client_id part of your organization\'s API key',
clientSecret: 'Client_secret part of your organization\'s API key',
})
.action(async (clientId: string, clientSecret: string, options: program.OptionValues) => {
await this.exitIfAuthed();
const command = new LoginCommand(this.main.authService, this.main.apiService, this.main.i18nService,
this.main.environmentService, this.main.passwordGenerationService, this.main.cryptoFunctionService,
this.main.platformUtilsService, this.main.userService, this.main.cryptoService,
this.main.policyService, 'connector', this.main.loginSyncService, this.main.keyConnectorService);
program
.command("login [clientId] [clientSecret]")
.description("Log into an organization account.", {
clientId: "Client_id part of your organization's API key",
clientSecret: "Client_secret part of your organization's API key",
})
.action(async (clientId: string, clientSecret: string, options: program.OptionValues) => {
await this.exitIfAuthed();
const command = new LoginCommand(
this.main.authService,
this.main.apiService,
this.main.i18nService,
this.main.environmentService,
this.main.passwordGenerationService,
this.main.cryptoFunctionService,
this.main.platformUtilsService,
this.main.userService,
this.main.cryptoService,
this.main.policyService,
"connector",
this.main.loginSyncService,
this.main.keyConnectorService
);
if (!Utils.isNullOrWhitespace(clientId)) {
process.env.BW_CLIENTID = clientId;
}
if (!Utils.isNullOrWhitespace(clientSecret)) {
process.env.BW_CLIENTSECRET = clientSecret;
}
options = Object.assign(options ?? {}, { apikey: true }); // force apikey use
const response = await command.run(null, null, options);
this.processResponse(response);
});
program
.command('logout')
.description('Log out of the current user account.')
.on('--help', () => {
writeLn('\n Examples:');
writeLn('');
writeLn(' bwdc logout');
writeLn('', true);
})
.action(async () => {
await this.exitIfNotAuthed();
const command = new LogoutCommand(this.main.authService, this.main.i18nService,
async () => await this.main.logout());
const response = await command.run();
this.processResponse(response);
});
program
.command('test')
.description('Test a simulated sync.')
.option('-l, --last', 'Since the last successful sync.')
.on('--help', () => {
writeLn('\n Examples:');
writeLn('');
writeLn(' bwdc test');
writeLn(' bwdc test --last');
writeLn('', true);
})
.action(async (options: program.OptionValues) => {
await this.exitIfNotAuthed();
const command = new TestCommand(this.main.syncService, this.main.i18nService);
const response = await command.run(options);
this.processResponse(response);
});
program
.command('sync')
.description('Sync the directory.')
.on('--help', () => {
writeLn('\n Examples:');
writeLn('');
writeLn(' bwdc sync');
writeLn('', true);
})
.action(async () => {
await this.exitIfNotAuthed();
const command = new SyncCommand(this.main.syncService, this.main.i18nService);
const response = await command.run();
this.processResponse(response);
});
program
.command('last-sync <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(clientId)) {
process.env.BW_CLIENTID = clientId;
}
}
async exitIfAuthed() {
const authed = await this.apiKeyService.isAuthenticated();
if (authed) {
const type = await this.apiKeyService.getEntityType();
const id = await this.apiKeyService.getEntityId();
this.processResponse(Response.error('You are already logged in as ' + type + '.' + id + '.'), true);
if (!Utils.isNullOrWhitespace(clientSecret)) {
process.env.BW_CLIENTSECRET = clientSecret;
}
}
async exitIfNotAuthed() {
const authed = await this.apiKeyService.isAuthenticated();
if (!authed) {
this.processResponse(Response.error('You are not logged in.'), true);
}
options = Object.assign(options ?? {}, { apikey: true }); // force apikey use
const response = await command.run(null, null, options);
this.processResponse(response);
});
program
.command("logout")
.description("Log out of the current user account.")
.on("--help", () => {
writeLn("\n Examples:");
writeLn("");
writeLn(" bwdc logout");
writeLn("", true);
})
.action(async () => {
await this.exitIfNotAuthed();
const command = new LogoutCommand(
this.main.authService,
this.main.i18nService,
async () => await this.main.logout()
);
const response = await command.run();
this.processResponse(response);
});
program
.command("test")
.description("Test a simulated sync.")
.option("-l, --last", "Since the last successful sync.")
.on("--help", () => {
writeLn("\n Examples:");
writeLn("");
writeLn(" bwdc test");
writeLn(" bwdc test --last");
writeLn("", true);
})
.action(async (options: program.OptionValues) => {
await this.exitIfNotAuthed();
const command = new TestCommand(this.main.syncService, this.main.i18nService);
const response = await command.run(options);
this.processResponse(response);
});
program
.command("sync")
.description("Sync the directory.")
.on("--help", () => {
writeLn("\n Examples:");
writeLn("");
writeLn(" bwdc sync");
writeLn("", true);
})
.action(async () => {
await this.exitIfNotAuthed();
const command = new SyncCommand(this.main.syncService, this.main.i18nService);
const response = await command.run();
this.processResponse(response);
});
program
.command("last-sync <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);
$font-family-sans-serif: 'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
$theme-colors: (
"primary": #175ddc,
"primary-accent": #1252a3,
"danger": #dd4b39,
"success": #00a65a,
"info": #555555,
"warning": #bf7e16,
"secondary": #ced4da,
"secondary-alt": #1a3b66,
);
$font-family-sans-serif: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
$h1-font-size: 2rem;
$h2-font-size: 1.3rem;
@@ -8,13 +18,13 @@ $h4-font-size: 1rem;
$h5-font-size: 1rem;
$h6-font-size: 1rem;
$primary: map_get($theme-colors, 'primary');
$primary-accent: map_get($theme-colors, 'primary-accent');
$success: map_get($theme-colors, 'success');
$info: map_get($theme-colors, 'info');
$warning: map_get($theme-colors, 'warning');
$danger: map_get($theme-colors, 'danger');
$secondary: map_get($theme-colors, 'secondary');
$secondary-alt: map_get($theme-colors, 'secondary-alt');
$primary: map_get($theme-colors, "primary");
$primary-accent: map_get($theme-colors, "primary-accent");
$success: map_get($theme-colors, "success");
$info: map_get($theme-colors, "info");
$warning: map_get($theme-colors, "warning");
$danger: map_get($theme-colors, "danger");
$secondary: map_get($theme-colors, "secondary");
$secondary-alt: map_get($theme-colors, "secondary-alt");
@import "~bootstrap/scss/bootstrap.scss";

View File

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

View File

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

View File

@@ -1,127 +1,128 @@
$fa-font-path: "~font-awesome/fonts";
@import "~font-awesome/scss/font-awesome.scss";
@import '~ngx-toastr/toastr';
@import "~ngx-toastr/toastr";
@import "~bootstrap/scss/_variables.scss";
.toast-container {
.toast-close-button {
font-size: 18px;
margin-right: 4px;
}
.ngx-toastr {
align-items: center;
background-image: none !important;
border-radius: $border-radius;
box-shadow: 0 0 8px rgba(0, 0, 0, 0.35);
display: flex;
padding: 15px;
.toast-close-button {
font-size: 18px;
margin-right: 4px;
position: absolute;
right: 5px;
top: 0;
}
.ngx-toastr {
align-items: center;
background-image: none !important;
border-radius: $border-radius;
box-shadow: 0 0 8px rgba(0, 0, 0, 0.35);
display: flex;
padding: 15px;
.toast-close-button {
position: absolute;
right: 5px;
top: 0;
}
&:hover {
box-shadow: 0 0 10px rgba(0, 0, 0, 0.6);
}
.icon i::before {
float: left;
font-style: normal;
font-family: FontAwesome;
font-size: 25px;
line-height: 20px;
padding-right: 15px;
}
.toast-message {
p {
margin-bottom: 0.5rem;
&:last-child {
margin-bottom: 0;
}
}
}
&.toast-danger, &.toast-error {
background-color: $danger;
.icon i::before {
content: "\f0e7";
}
}
&.toast-warning {
background-color: $warning;
.icon i::before {
content: "\f071";
}
}
&.toast-info {
background-color: $info;
.icon i:before {
content: "\f05a";
}
}
&.toast-success {
background-color: $success;
.icon i:before {
content: "\f00C";
}
}
&:hover {
box-shadow: 0 0 10px rgba(0, 0, 0, 0.6);
}
.icon i::before {
float: left;
font-style: normal;
font-family: FontAwesome;
font-size: 25px;
line-height: 20px;
padding-right: 15px;
}
.toast-message {
p {
margin-bottom: 0.5rem;
&:last-child {
margin-bottom: 0;
}
}
}
&.toast-danger,
&.toast-error {
background-color: $danger;
.icon i::before {
content: "\f0e7";
}
}
&.toast-warning {
background-color: $warning;
.icon i::before {
content: "\f071";
}
}
&.toast-info {
background-color: $info;
.icon i:before {
content: "\f05a";
}
}
&.toast-success {
background-color: $success;
.icon i:before {
content: "\f00C";
}
}
}
}
@keyframes modalshow {
0% {
opacity: 0;
transform: translate(0, -25%);
}
0% {
opacity: 0;
transform: translate(0, -25%);
}
100% {
opacity: 1;
transform: translate(0, 0);
}
100% {
opacity: 1;
transform: translate(0, 0);
}
}
@keyframes backdropshow {
0% {
opacity: 0;
}
0% {
opacity: 0;
}
100% {
opacity: $modal-backdrop-opacity;
}
100% {
opacity: $modal-backdrop-opacity;
}
}
.modal {
display: block !important;
opacity: 1 !important;
display: block !important;
opacity: 1 !important;
}
.modal-dialog {
.modal.fade & {
transform: initial !important;
animation: modalshow 0.3s ease-in;
}
.modal.show & {
transform: initial !important;
}
transform: translate(0, 0);
.modal.fade & {
transform: initial !important;
animation: modalshow 0.3s ease-in;
}
.modal.show & {
transform: initial !important;
}
transform: translate(0, 0);
}
.modal-backdrop {
&.fade {
animation: backdropshow 0.1s ease-in;
}
opacity: $modal-backdrop-opacity !important;
&.fade {
animation: backdropshow 0.1s ease-in;
}
opacity: $modal-backdrop-opacity !important;
}

View File

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

View File

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

View File

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

View File

@@ -1,91 +1,95 @@
import { SyncConfiguration } from '../models/syncConfiguration';
import { SyncConfiguration } from "../models/syncConfiguration";
import { GroupEntry } from '../models/groupEntry';
import { UserEntry } from '../models/userEntry';
import { GroupEntry } from "../models/groupEntry";
import { UserEntry } from "../models/userEntry";
export abstract class BaseDirectoryService {
protected createDirectoryQuery(filter: string) {
if (filter == null || filter === '') {
return null;
}
const mainParts = filter.split('|');
if (mainParts.length < 2 || mainParts[1] == null || mainParts[1].trim() === '') {
return null;
}
return mainParts[1].trim();
protected createDirectoryQuery(filter: string) {
if (filter == null || filter === "") {
return null;
}
protected createCustomSet(filter: string): [boolean, Set<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 {
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];
const mainParts = filter.split("|");
if (mainParts.length < 2 || mainParts[1] == null || mainParts[1].trim() === "") {
return null;
}
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];
return mainParts[1].trim();
}
if (excluded && set.has(cleanResult)) {
return true;
} else if (!excluded && !set.has(cleanResult)) {
return true;
}
}
return false;
protected createCustomSet(filter: string): [boolean, Set<string>] {
if (filter == null || filter === "") {
return null;
}
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;
});
const mainParts = filter.split("|");
if (mainParts.length < 1 || mainParts[0] == null || mainParts[0].trim() === "") {
return null;
}
protected forceGroup(force: boolean, users: UserEntry[]): boolean {
return force || (users != null && users.filter(u => !u.deleted && !u.disabled).length > 0);
const parts = mainParts[0].split(":");
if (parts.length !== 2) {
return null;
}
const keyword = parts[0].trim().toLowerCase();
let exclude = true;
if (keyword === "include") {
exclude = false;
} else if (keyword === "exclude") {
exclude = true;
} else {
return null;
}
const set = new Set<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 { AzureConfiguration } from '../models/azureConfiguration';
import { GSuiteConfiguration } from '../models/gsuiteConfiguration';
import { LdapConfiguration } from '../models/ldapConfiguration';
import { OktaConfiguration } from '../models/oktaConfiguration';
import { OneLoginConfiguration } from '../models/oneLoginConfiguration';
import { SyncConfiguration } from '../models/syncConfiguration';
import { StorageService } from "jslib-common/abstractions/storage.service";
import { AzureConfiguration } from "../models/azureConfiguration";
import { GSuiteConfiguration } from "../models/gsuiteConfiguration";
import { LdapConfiguration } from "../models/ldapConfiguration";
import { OktaConfiguration } from "../models/oktaConfiguration";
import { OneLoginConfiguration } from "../models/oneLoginConfiguration";
import { SyncConfiguration } from "../models/syncConfiguration";
const StoredSecurely = '[STORED SECURELY]';
const StoredSecurely = "[STORED SECURELY]";
const Keys = {
ldap: 'ldapPassword',
gsuite: 'gsuitePrivateKey',
azure: 'azureKey',
okta: 'oktaToken',
oneLogin: 'oneLoginClientSecret',
directoryConfigPrefix: 'directoryConfig_',
sync: 'syncConfig',
directoryType: 'directoryType',
userDelta: 'userDeltaToken',
groupDelta: 'groupDeltaToken',
lastUserSync: 'lastUserSync',
lastGroupSync: 'lastGroupSync',
lastSyncHash: 'lastSyncHash',
organizationId: 'organizationId',
ldap: "ldapPassword",
gsuite: "gsuitePrivateKey",
azure: "azureKey",
okta: "oktaToken",
oneLogin: "oneLoginClientSecret",
directoryConfigPrefix: "directoryConfig_",
sync: "syncConfig",
directoryType: "directoryType",
userDelta: "userDeltaToken",
groupDelta: "groupDeltaToken",
lastUserSync: "lastUserSync",
lastGroupSync: "lastGroupSync",
lastSyncHash: "lastSyncHash",
organizationId: "organizationId",
};
export class ConfigurationService {
constructor(private storageService: StorageService, private secureStorageService: StorageService,
private useSecureStorageForSecrets = true) { }
constructor(
private storageService: StorageService,
private secureStorageService: StorageService,
private useSecureStorageForSecrets = true
) {}
async getDirectory<T>(type: DirectoryType): Promise<T> {
const config = await this.storageService.get<T>(Keys.directoryConfigPrefix + type);
if (config == null) {
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 getDirectory<T>(type: DirectoryType): Promise<T> {
const config = await this.storageService.get<T>(Keys.directoryConfigPrefix + type);
if (config == null) {
return config;
}
async saveDirectory(type: DirectoryType,
config: LdapConfiguration | GSuiteConfiguration | AzureConfiguration | OktaConfiguration |
OneLoginConfiguration): Promise<any> {
const savedConfig: any = Object.assign({}, config);
if (this.useSecureStorageForSecrets) {
switch (type) {
case DirectoryType.Ldap:
if (savedConfig.password == null) {
await this.secureStorageService.remove(Keys.ldap);
} else {
await this.secureStorageService.save(Keys.ldap, savedConfig.password);
savedConfig.password = StoredSecurely;
}
break;
case DirectoryType.AzureActiveDirectory:
if (savedConfig.key == null) {
await this.secureStorageService.remove(Keys.azure);
} else {
await this.secureStorageService.save(Keys.azure, savedConfig.key);
savedConfig.key = StoredSecurely;
}
break;
case DirectoryType.Okta:
if (savedConfig.token == null) {
await this.secureStorageService.remove(Keys.okta);
} else {
await this.secureStorageService.save(Keys.okta, savedConfig.token);
savedConfig.token = StoredSecurely;
}
break;
case DirectoryType.GSuite:
if (savedConfig.privateKey == null) {
await this.secureStorageService.remove(Keys.gsuite);
} else {
(config as GSuiteConfiguration).privateKey = savedConfig.privateKey =
savedConfig.privateKey.replace(/\\n/g, '\n');
await this.secureStorageService.save(Keys.gsuite, savedConfig.privateKey);
savedConfig.privateKey = StoredSecurely;
}
break;
case DirectoryType.OneLogin:
if (savedConfig.clientSecret == null) {
await this.secureStorageService.remove(Keys.oneLogin);
} else {
await this.secureStorageService.save(Keys.oneLogin, savedConfig.clientSecret);
savedConfig.clientSecret = StoredSecurely;
}
break;
}
}
await this.storageService.save(Keys.directoryConfigPrefix + type, savedConfig);
if (this.useSecureStorageForSecrets) {
switch (type) {
case DirectoryType.Ldap:
(config as any).password = await this.secureStorageService.get<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,
config:
| LdapConfiguration
| GSuiteConfiguration
| AzureConfiguration
| OktaConfiguration
| OneLoginConfiguration
): Promise<any> {
const savedConfig: any = Object.assign({}, config);
if (this.useSecureStorageForSecrets) {
switch (type) {
case DirectoryType.Ldap:
if (savedConfig.password == null) {
await this.secureStorageService.remove(Keys.ldap);
} else {
await this.secureStorageService.save(Keys.ldap, savedConfig.password);
savedConfig.password = StoredSecurely;
}
break;
case DirectoryType.AzureActiveDirectory:
if (savedConfig.key == null) {
await this.secureStorageService.remove(Keys.azure);
} else {
await this.secureStorageService.save(Keys.azure, savedConfig.key);
savedConfig.key = StoredSecurely;
}
break;
case DirectoryType.Okta:
if (savedConfig.token == null) {
await this.secureStorageService.remove(Keys.okta);
} else {
await this.secureStorageService.save(Keys.okta, savedConfig.token);
savedConfig.token = StoredSecurely;
}
break;
case DirectoryType.GSuite:
if (savedConfig.privateKey == null) {
await this.secureStorageService.remove(Keys.gsuite);
} else {
(config as GSuiteConfiguration).privateKey = savedConfig.privateKey =
savedConfig.privateKey.replace(/\\n/g, "\n");
await this.secureStorageService.save(Keys.gsuite, savedConfig.privateKey);
savedConfig.privateKey = StoredSecurely;
}
break;
case DirectoryType.OneLogin:
if (savedConfig.clientSecret == null) {
await this.secureStorageService.remove(Keys.oneLogin);
} else {
await this.secureStorageService.save(Keys.oneLogin, savedConfig.clientSecret);
savedConfig.clientSecret = StoredSecurely;
}
break;
}
}
await this.storageService.save(Keys.directoryConfigPrefix + type, savedConfig);
}
getSync(): Promise<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.get<SyncConfiguration>(Keys.sync);
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();
}
saveSync(config: SyncConfiguration) {
return this.storageService.save(Keys.sync, config);
if (id == null) {
return this.storageService.remove(Keys.organizationId);
} else {
return this.storageService.save(Keys.organizationId, id);
}
}
getDirectoryType(): Promise<DirectoryType> {
return this.storageService.get<DirectoryType>(Keys.directoryType);
}
async saveDirectoryType(type: DirectoryType) {
const currentType = await this.getDirectoryType();
if (type !== currentType) {
await this.clearStatefulSettings();
}
return this.storageService.save(Keys.directoryType, type);
}
getUserDeltaToken(): Promise<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);
}
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 { UserEntry } from '../models/userEntry';
import { GroupEntry } from "../models/groupEntry";
import { UserEntry } from "../models/userEntry";
export interface IDirectoryService {
getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]>;
getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]>;
}

View File

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

View File

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

View File

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

View File

@@ -1,450 +1,506 @@
import * as fs from 'fs';
import * as ldap from 'ldapjs';
import * as fs from "fs";
import * as ldap from "ldapjs";
import { checkServerIdentity, PeerCertificate } from 'tls';
import { checkServerIdentity, PeerCertificate } from "tls";
import { DirectoryType } from '../enums/directoryType';
import { DirectoryType } from "../enums/directoryType";
import { GroupEntry } from '../models/groupEntry';
import { LdapConfiguration } from '../models/ldapConfiguration';
import { SyncConfiguration } from '../models/syncConfiguration';
import { UserEntry } from '../models/userEntry';
import { GroupEntry } from "../models/groupEntry";
import { LdapConfiguration } from "../models/ldapConfiguration";
import { SyncConfiguration } from "../models/syncConfiguration";
import { UserEntry } from "../models/userEntry";
import { ConfigurationService } from './configuration.service';
import { IDirectoryService } from './directory.service';
import { ConfigurationService } from "./configuration.service";
import { IDirectoryService } from "./directory.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { Utils } from 'jslib-common/misc/utils';
import { Utils } from "jslib-common/misc/utils";
const UserControlAccountDisabled = 2;
export class LdapDirectoryService implements IDirectoryService {
private client: ldap.Client;
private dirConfig: LdapConfiguration;
private syncConfig: SyncConfiguration;
private client: ldap.Client;
private dirConfig: LdapConfiguration;
private syncConfig: SyncConfiguration;
constructor(private configurationService: ConfigurationService, private logService: LogService,
private i18nService: I18nService) { }
constructor(
private configurationService: ConfigurationService,
private logService: LogService,
private i18nService: I18nService
) {}
async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> {
const type = await this.configurationService.getDirectoryType();
if (type !== DirectoryType.Ldap) {
return;
}
this.dirConfig = await this.configurationService.getDirectory<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];
async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> {
const type = await this.configurationService.getDirectoryType();
if (type !== DirectoryType.Ldap) {
return;
}
private async getUsers(force: boolean): Promise<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;
}
this.dirConfig = await this.configurationService.getDirectory<LdapConfiguration>(
DirectoryType.Ldap
);
if (this.dirConfig == null) {
return;
}
private buildUser(searchEntry: any, deleted: boolean): UserEntry {
const user = new UserEntry();
user.referenceId = searchEntry.objectName;
user.deleted = deleted;
if (user.referenceId == null) {
return null;
}
user.externalId = this.getExternalId(searchEntry, user.referenceId);
user.disabled = this.entryDisabled(searchEntry);
user.email = this.getAttr(searchEntry, this.syncConfig.userEmailAttribute);
if (user.email == null && this.syncConfig.useEmailPrefixSuffix &&
this.syncConfig.emailPrefixAttribute != null && this.syncConfig.emailSuffix != null) {
const prefixAttr = this.getAttr(searchEntry, this.syncConfig.emailPrefixAttribute);
if (prefixAttr != null) {
user.email = prefixAttr + this.syncConfig.emailSuffix;
}
}
if (user.email != null) {
user.email = user.email.trim().toLowerCase();
}
if (!user.deleted && (user.email == null || user.email.trim() === '')) {
return null;
}
return user;
this.syncConfig = await this.configurationService.getSync();
if (this.syncConfig == null) {
return;
}
private async getGroups(force: boolean): Promise<GroupEntry[]> {
const entries: GroupEntry[] = [];
await this.bind();
const lastSync = await this.configurationService.getLastUserSyncDate();
const originalFilter = this.buildBaseFilter(this.syncConfig.groupObjectClass, this.syncConfig.groupFilter);
let filter = originalFilter;
const revisionFilter = this.buildRevisionFilter(filter, force, lastSync);
const searchSinceRevision = filter !== revisionFilter;
filter = revisionFilter;
let users: UserEntry[];
if (this.syncConfig.users) {
users = await this.getUsers(force);
}
const path = this.makeSearchPath(this.syncConfig.groupPath);
this.logService.info('Group search: ' + path + ' => ' + filter);
let groups: GroupEntry[];
if (this.syncConfig.groups) {
let groupForce = force;
if (!groupForce && users != null) {
const activeUsers = users.filter((u) => !u.deleted && !u.disabled);
groupForce = activeUsers.length > 0;
}
groups = await this.getGroups(groupForce);
}
let groupSearchEntries: any[] = [];
const initialSearchGroupIds = await this.search<string>(path, filter, (se: any) => {
groupSearchEntries.push(se);
return se.objectName;
await this.unbind();
return [groups, users];
}
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) {
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;
res.on("searchEntry", (entry) => {
const e = processEntry(entry);
if (e != null) {
entries.push(e);
}
});
for (const se of groupSearchEntries) {
const group = this.buildGroup(se, userIdMap);
if (group != null) {
entries.push(group);
}
res.on("end", (result) => {
resolve(entries);
});
});
});
}
private async bind(): Promise<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;
}
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]);
if (!this.dirConfig.startTls) {
if (
this.dirConfig.sslCaPath != null &&
this.dirConfig.sslCaPath !== "" &&
fs.existsSync(this.dirConfig.sslCaPath)
) {
tlsOptions.ca = [fs.readFileSync(this.dirConfig.sslCaPath)];
}
if (
this.dirConfig.sslCertPath != null &&
this.dirConfig.sslCertPath !== "" &&
fs.existsSync(this.dirConfig.sslCertPath)
) {
tlsOptions.cert = fs.readFileSync(this.dirConfig.sslCertPath);
}
if (
this.dirConfig.sslKeyPath != null &&
this.dirConfig.sslKeyPath !== "" &&
fs.existsSync(this.dirConfig.sslKeyPath)
) {
tlsOptions.key = fs.readFileSync(this.dirConfig.sslKeyPath);
}
} else {
return referenceId;
if (
this.dirConfig.tlsCaPath != null &&
this.dirConfig.tlsCaPath !== "" &&
fs.existsSync(this.dirConfig.tlsCaPath)
) {
tlsOptions.ca = [fs.readFileSync(this.dirConfig.tlsCaPath)];
}
}
}
}
private buildBaseFilter(objectClass: string, subFilter: string): string {
let filter = this.buildObjectClassFilter(objectClass);
if (subFilter != null && subFilter.trim() !== '') {
filter = '(&' + filter + subFilter + ')';
}
return filter;
}
tlsOptions.checkServerIdentity = this.checkServerIdentityAltNames;
options.tlsOptions = tlsOptions;
private buildObjectClassFilter(objectClass: string): string {
return '(&(objectClass=' + objectClass + '))';
}
this.client = ldap.createClient(options);
private buildRevisionFilter(baseFilter: string, force: boolean, lastRevisionDate: Date) {
const revisionAttr = this.syncConfig.revisionDateAttribute;
if (!force && lastRevisionDate != null && revisionAttr != null && revisionAttr.trim() !== '') {
const dateString = lastRevisionDate.toISOString().replace(/[-:T]/g, '').substr(0, 16) + 'Z';
baseFilter = '(&' + baseFilter + '(' + revisionAttr + '>=' + dateString + '))';
}
const user =
this.dirConfig.username == null || this.dirConfig.username.trim() === ""
? null
: this.dirConfig.username;
const pass =
this.dirConfig.password == null || this.dirConfig.password.trim() === ""
? null
: this.dirConfig.password;
return baseFilter;
}
if (user == null || pass == null) {
reject(this.i18nService.t("usernamePasswordNotConfigured"));
return;
}
private makeSearchPath(pathPrefix: string) {
if (this.dirConfig.rootPath.toLowerCase().indexOf('dc=') === -1) {
return pathPrefix;
}
if (this.dirConfig.rootPath != null && this.dirConfig.rootPath.trim() !== '') {
const trimmedRootPath = this.dirConfig.rootPath.trim().toLowerCase();
let path = trimmedRootPath.substr(trimmedRootPath.indexOf('dc='));
if (pathPrefix != null && pathPrefix.trim() !== '') {
path = pathPrefix.trim() + ',' + path;
}
return path;
}
return null;
}
private getAttrObj(searchEntry: any, attr: string): any {
if (searchEntry == null || searchEntry.attributes == null) {
return null;
}
const attrs = searchEntry.attributes.filter((a: any) => a.type === attr);
if (attrs == null || attrs.length === 0 || attrs[0].vals == null || attrs[0].vals.length === 0) {
return null;
}
return attrs[0];
}
private getAttrVals(searchEntry: any, attr: string): string[] {
const obj = this.getAttrObj(searchEntry, attr);
if (obj == null) {
return null;
}
return obj.vals;
}
private getAttr(searchEntry: any, attr: string): string {
const vals = this.getAttrVals(searchEntry, attr);
if (vals == null) {
return null;
}
return vals[0];
}
private entryDisabled(searchEntry: any): boolean {
const c = this.getAttr(searchEntry, 'userAccountControl');
if (c != null) {
try {
const control = parseInt(c, null);
// tslint:disable-next-line
return (control & UserControlAccountDisabled) === UserControlAccountDisabled;
} catch (e) {
this.logService.error(e);
}
}
return false;
}
private async search<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);
});
if (this.dirConfig.startTls && this.dirConfig.ssl) {
this.client.starttls(options.tlsOptions, undefined, (err, res) => {
if (err != null) {
reject(err.message);
} else {
this.client.bind(user, pass, (err2) => {
if (err2 != null) {
reject(err2.message);
} else {
resolve();
}
});
}
});
}
private async bind(): Promise<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) {
if (this.dirConfig.sslCaPath != null && this.dirConfig.sslCaPath !== '' &&
fs.existsSync(this.dirConfig.sslCaPath)) {
tlsOptions.ca = [fs.readFileSync(this.dirConfig.sslCaPath)];
}
if (this.dirConfig.sslCertPath != null && this.dirConfig.sslCertPath !== '' &&
fs.existsSync(this.dirConfig.sslCertPath)) {
tlsOptions.cert = fs.readFileSync(this.dirConfig.sslCertPath);
}
if (this.dirConfig.sslKeyPath != null && this.dirConfig.sslKeyPath !== '' &&
fs.existsSync(this.dirConfig.sslKeyPath)) {
tlsOptions.key = fs.readFileSync(this.dirConfig.sslKeyPath);
}
} else {
if (this.dirConfig.tlsCaPath != null && this.dirConfig.tlsCaPath !== '' &&
fs.existsSync(this.dirConfig.tlsCaPath)) {
tlsOptions.ca = [fs.readFileSync(this.dirConfig.tlsCaPath)];
}
}
}
tlsOptions.checkServerIdentity = this.checkServerIdentityAltNames;
options.tlsOptions = tlsOptions;
this.client = ldap.createClient(options);
const user = this.dirConfig.username == null || this.dirConfig.username.trim() === '' ? null :
this.dirConfig.username;
const pass = this.dirConfig.password == null || this.dirConfig.password.trim() === '' ? null :
this.dirConfig.password;
if (user == null || pass == null) {
reject(this.i18nService.t('usernamePasswordNotConfigured'));
return;
}
if (this.dirConfig.startTls && this.dirConfig.ssl) {
this.client.starttls(options.tlsOptions, undefined, (err, res) => {
if (err != null) {
reject(err.message);
} else {
this.client.bind(user, pass, err2 => {
if (err2 != null) {
reject(err2.message);
} else {
resolve();
}
});
}
});
} else {
this.client.bind(user, pass, err => {
if (err != null) {
reject(err.message);
} else {
resolve();
}
});
}
} else {
this.client.bind(user, pass, (err) => {
if (err != null) {
reject(err.message);
} else {
resolve();
}
});
}
}
});
}
private async unbind(): Promise<void> {
return new Promise((resolve, reject) => {
this.client.unbind(err => {
if (err != null) {
reject(err);
} else {
resolve();
}
});
});
}
private bufToGuid(buf: Buffer) {
const arr = new Uint8Array(buf);
const p1 = arr.slice(0, 4).reverse().buffer;
const p2 = arr.slice(4, 6).reverse().buffer;
const p3 = arr.slice(6, 8).reverse().buffer;
const p4 = arr.slice(8, 10).buffer;
const p5 = arr.slice(10).buffer;
const guid = Utils.fromBufferToHex(p1) + '-' + Utils.fromBufferToHex(p2) + '-' + Utils.fromBufferToHex(p3) +
'-' + Utils.fromBufferToHex(p4) + '-' + Utils.fromBufferToHex(p5);
return guid.toLowerCase();
}
private checkServerIdentityAltNames(host: string, cert: PeerCertificate) {
// Fixes the cert representation when subject is empty and altNames are present
// Required for node versions < 12.14.1 (which could be used for bwdc cli)
// Adapted from: https://github.com/auth0/ad-ldap-connector/commit/1f4dd2be6ed93dda591dd31ed5483a9b452a8d2a
// See https://github.com/nodejs/node/issues/11771 for details
if (cert && cert.subject == null && /(IP|DNS|URL)/.test(cert.subjectaltname)) {
cert.subject = {
C: null,
ST: null,
L: null,
O: null,
OU: null,
CN: null,
};
private async unbind(): Promise<void> {
return new Promise((resolve, reject) => {
this.client.unbind((err) => {
if (err != null) {
reject(err);
} else {
resolve();
}
});
});
}
return checkServerIdentity(host, cert);
private bufToGuid(buf: Buffer) {
const arr = new Uint8Array(buf);
const p1 = arr.slice(0, 4).reverse().buffer;
const p2 = arr.slice(4, 6).reverse().buffer;
const p3 = arr.slice(6, 8).reverse().buffer;
const p4 = arr.slice(8, 10).buffer;
const p5 = arr.slice(10).buffer;
const guid =
Utils.fromBufferToHex(p1) +
"-" +
Utils.fromBufferToHex(p2) +
"-" +
Utils.fromBufferToHex(p3) +
"-" +
Utils.fromBufferToHex(p4) +
"-" +
Utils.fromBufferToHex(p5);
return guid.toLowerCase();
}
private checkServerIdentityAltNames(host: string, cert: PeerCertificate) {
// Fixes the cert representation when subject is empty and altNames are present
// Required for node versions < 12.14.1 (which could be used for bwdc cli)
// Adapted from: https://github.com/auth0/ad-ldap-connector/commit/1f4dd2be6ed93dda591dd31ed5483a9b452a8d2a
// See https://github.com/nodejs/node/issues/11771 for details
if (cert && cert.subject == null && /(IP|DNS|URL)/.test(cert.subjectaltname)) {
cert.subject = {
C: null,
ST: null,
L: null,
O: null,
OU: null,
CN: null,
};
}
return checkServerIdentity(host, cert);
}
}

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

View File

@@ -1,17 +1,30 @@
import { EnvironmentService } from 'jslib-common/abstractions/environment.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { TokenService } from 'jslib-common/abstractions/token.service';
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { TokenService } from "jslib-common/abstractions/token.service";
import { NodeApiService as NodeApiServiceBase } from 'jslib-node/services/nodeApi.service';
import { NodeApiService as NodeApiServiceBase } from "jslib-node/services/nodeApi.service";
export class NodeApiService extends NodeApiServiceBase {
constructor(tokenService: TokenService, platformUtilsService: PlatformUtilsService, environmentService: EnvironmentService,
private refreshTokenCallback: () => Promise<void>, logoutCallback: (expired: boolean) => Promise<void>,
customUserAgent: string = null, apiKeyRefresh: (clientId: string, clientSecret: string) => Promise<any>) {
super(tokenService, platformUtilsService, environmentService, logoutCallback, customUserAgent, apiKeyRefresh);
}
constructor(
tokenService: TokenService,
platformUtilsService: PlatformUtilsService,
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> {
return this.refreshTokenCallback();
}
doRefreshToken(): Promise<void> {
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 { OktaConfiguration } from '../models/oktaConfiguration';
import { SyncConfiguration } from '../models/syncConfiguration';
import { UserEntry } from '../models/userEntry';
import { GroupEntry } from "../models/groupEntry";
import { OktaConfiguration } from "../models/oktaConfiguration";
import { SyncConfiguration } from "../models/syncConfiguration";
import { UserEntry } from "../models/userEntry";
import { BaseDirectoryService } from './baseDirectory.service';
import { ConfigurationService } from './configuration.service';
import { IDirectoryService } from './directory.service';
import { BaseDirectoryService } from "./baseDirectory.service";
import { ConfigurationService } from "./configuration.service";
import { IDirectoryService } from "./directory.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import * as https from 'https';
import * as https from "https";
const DelayBetweenBuildGroupCallsInMilliseconds = 500;
export class OktaDirectoryService extends BaseDirectoryService implements IDirectoryService {
private dirConfig: OktaConfiguration;
private syncConfig: SyncConfiguration;
private lastBuildGroupCall: number;
private dirConfig: OktaConfiguration;
private syncConfig: SyncConfiguration;
private lastBuildGroupCall: number;
constructor(private configurationService: ConfigurationService, private logService: LogService,
private i18nService: I18nService) {
super();
constructor(
private configurationService: ConfigurationService,
private logService: LogService,
private i18nService: I18nService
) {
super();
}
async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> {
const type = await this.configurationService.getDirectoryType();
if (type !== DirectoryType.Okta) {
return;
}
async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> {
const type = await this.configurationService.getDirectoryType();
if (type !== DirectoryType.Okta) {
return;
}
this.dirConfig = await this.configurationService.getDirectory<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];
this.dirConfig = await this.configurationService.getDirectory<OktaConfiguration>(
DirectoryType.Okta
);
if (this.dirConfig == null) {
return;
}
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.syncConfig = await this.configurationService.getSync();
if (this.syncConfig == null) {
return;
}
this.logService.info('Querying users.');
const usersPromise = this.apiGetMany('users?filter=' + this.encodeUrlParameter(oktaFilter))
.then((users: any[]) => {
for (const user of users) {
const entry = this.buildUser(user);
if (entry != null && !this.filterOutResult(setFilter, entry.email)) {
entries.push(entry);
}
if (this.dirConfig.orgUrl == null || this.dirConfig.token == null) {
throw new Error(this.i18nService.t("dirConfigIncomplete"));
}
let users: UserEntry[];
if (this.syncConfig.users) {
users = await this.getUsers(force);
}
let groups: GroupEntry[];
if (this.syncConfig.groups) {
const setFilter = this.createCustomSet(this.syncConfig.groupFilter);
groups = await this.getGroups(this.forceGroup(force, users), setFilter);
users = this.filterUsersFromGroupsSet(users, groups, setFilter, this.syncConfig);
}
return [groups, users];
}
private async getUsers(force: boolean): Promise<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);
}
});
// Deactivated users have to be queried for separately, only when no filter is provided in the first query
let deactUsersPromise: any;
if (oktaFilter == null || oktaFilter.indexOf('lastUpdated ') === -1) {
let deactOktaFilter = 'status eq "DEPROVISIONED"';
if (oktaFilter != null) {
deactOktaFilter = '(' + oktaFilter + ') and ' + deactOktaFilter;
}
resolve([responseJson, headersMap]);
return;
}
deactUsersPromise = this.apiGetMany('users?filter=' + this.encodeUrlParameter(deactOktaFilter))
.then((users: any[]) => {
for (const user of users) {
const entry = this.buildUser(user);
if (entry != null && !this.filterOutResult(setFilter, entry.email)) {
entries.push(entry);
}
}
});
} else {
deactUsersPromise = Promise.resolve();
}
resolve([responseJson, null]);
});
await Promise.all([usersPromise, deactUsersPromise]);
return entries;
res.on("error", () => {
resolve(null);
});
}
);
});
}
private async apiGetMany(endpoint: string, currentData: any[] = []): Promise<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.");
}
private buildUser(user: any) {
const entry = new UserEntry();
entry.externalId = user.id;
entry.referenceId = user.id;
entry.email = user.profile.email != null ? user.profile.email.trim().toLowerCase() : null;
entry.deleted = user.status === 'DEPROVISIONED';
entry.disabled = user.status === 'SUSPENDED';
return entry;
if (response[0].length === 0) {
return currentData;
}
private async getGroups(force: boolean, setFilter: [boolean, Set<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;
currentData = currentData.concat(response[0]);
if (response[1] == null) {
return currentData;
}
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;
const linkHeader = response[1].get("link");
if (linkHeader == null || Array.isArray(linkHeader)) {
return currentData;
}
private buildOktaFilter(baseFilter: string, force: boolean, lastSync: Date) {
baseFilter = this.createDirectoryQuery(baseFilter);
baseFilter = baseFilter == null || baseFilter.trim() === '' ? null : baseFilter;
if (force || lastSync == null) {
return baseFilter;
let nextLink: string = null;
const linkHeaderParts = linkHeader.split(",");
for (const part of linkHeaderParts) {
if (part.indexOf('; rel="next"') > -1) {
const subParts = part.split(";");
if (subParts.length > 0 && subParts[0].indexOf("https://") > -1) {
nextLink = subParts[0].replace(">", "").replace("<", "").trim();
break;
}
const updatedFilter = 'lastUpdated gt "' + lastSync.toISOString() + '"';
if (baseFilter == null) {
return updatedFilter;
}
return '(' + baseFilter + ') and ' + updatedFilter;
}
}
private encodeUrlParameter(filter: string): string {
return filter == null ? '' : encodeURIComponent(filter);
}
private async apiGetCall(url: string): Promise<[any, Map<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);
if (nextLink == null) {
return 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 { OneLoginConfiguration } from '../models/oneLoginConfiguration';
import { SyncConfiguration } from '../models/syncConfiguration';
import { UserEntry } from '../models/userEntry';
import { GroupEntry } from "../models/groupEntry";
import { OneLoginConfiguration } from "../models/oneLoginConfiguration";
import { SyncConfiguration } from "../models/syncConfiguration";
import { UserEntry } from "../models/userEntry";
import { BaseDirectoryService } from './baseDirectory.service';
import { ConfigurationService } from './configuration.service';
import { IDirectoryService } from './directory.service';
import { BaseDirectoryService } from "./baseDirectory.service";
import { ConfigurationService } from "./configuration.service";
import { IDirectoryService } from "./directory.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
// Basic email validation: something@something.something
const ValidEmailRegex = /^\S+@\S+\.\S+$/;
export class OneLoginDirectoryService extends BaseDirectoryService implements IDirectoryService {
private dirConfig: OneLoginConfiguration;
private syncConfig: SyncConfiguration;
private accessToken: string;
private allUsers: any[] = [];
private dirConfig: OneLoginConfiguration;
private syncConfig: SyncConfiguration;
private accessToken: string;
private allUsers: any[] = [];
constructor(private configurationService: ConfigurationService, private logService: LogService,
private i18nService: I18nService) {
super();
constructor(
private configurationService: ConfigurationService,
private logService: LogService,
private i18nService: I18nService
) {
super();
}
async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> {
const type = await this.configurationService.getDirectoryType();
if (type !== DirectoryType.OneLogin) {
return;
}
async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> {
const type = await this.configurationService.getDirectoryType();
if (type !== DirectoryType.OneLogin) {
return;
}
this.dirConfig = await this.configurationService.getDirectory<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];
this.dirConfig = await this.configurationService.getDirectory<OneLoginConfiguration>(
DirectoryType.OneLogin
);
if (this.dirConfig == null) {
return;
}
private async getUsers(force: boolean): Promise<UserEntry[]> {
const entries: UserEntry[] = [];
const query = this.createDirectoryQuery(this.syncConfig.userFilter);
const setFilter = this.createCustomSet(this.syncConfig.userFilter);
this.logService.info('Querying users.');
this.allUsers = await this.apiGetMany('users' + (query != null ? '?' + query : ''));
this.allUsers.forEach(user => {
const entry = this.buildUser(user);
if (entry != null && !this.filterOutResult(setFilter, entry.email)) {
entries.push(entry);
}
});
return Promise.resolve(entries);
this.syncConfig = await this.configurationService.getSync();
if (this.syncConfig == null) {
return;
}
private buildUser(user: any) {
const entry = new UserEntry();
entry.externalId = user.id;
entry.referenceId = user.id;
entry.deleted = false;
entry.disabled = user.status === 2;
entry.email = user.email;
if (!this.validEmailAddress(entry.email) && user.username != null && user.username !== '') {
if (this.validEmailAddress(user.username)) {
entry.email = user.username;
} else if (this.syncConfig.useEmailPrefixSuffix && this.syncConfig.emailSuffix != null) {
entry.email = user.username + this.syncConfig.emailSuffix;
}
}
if (entry.email != null) {
entry.email = entry.email.trim().toLowerCase();
}
if (!this.validEmailAddress(entry.email)) {
return null;
}
return entry;
if (this.dirConfig.clientId == null || this.dirConfig.clientSecret == null) {
throw new Error(this.i18nService.t("dirConfigIncomplete"));
}
private async getGroups(force: boolean, setFilter: [boolean, Set<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);
this.accessToken = await this.getAccessToken();
if (this.accessToken == null) {
throw new Error("Could not get access token");
}
private buildGroup(group: any) {
const entry = new GroupEntry();
entry.externalId = group.id;
entry.referenceId = group.id;
entry.name = group.name;
if (this.allUsers != null) {
this.allUsers.forEach(user => {
if (user.role_id != null && user.role_id.indexOf(entry.referenceId) > -1) {
entry.userMemberExternalIds.add(user.id);
}
});
}
return entry;
let users: UserEntry[];
if (this.syncConfig.users) {
users = await this.getUsers(force);
}
private async getAccessToken() {
const response = await fetch(`https://api.${this.dirConfig.region}.onelogin.com/auth/oauth2/v2/token`, {
method: 'POST',
headers: new Headers({
'Authorization': 'Basic ' + btoa(this.dirConfig.clientId + ':' + this.dirConfig.clientSecret),
'Content-Type': 'application/json; charset=utf-8',
'Accept': 'application/json',
}),
body: JSON.stringify({
grant_type: 'client_credentials',
}),
});
if (response.status === 200) {
const responseJson = await response.json();
if (responseJson.access_token != null) {
return responseJson.access_token;
}
}
return null;
let groups: GroupEntry[];
if (this.syncConfig.groups) {
const setFilter = this.createCustomSet(this.syncConfig.groupFilter);
groups = await this.getGroups(this.forceGroup(force, users), setFilter);
users = this.filterUsersFromGroupsSet(users, groups, setFilter, this.syncConfig);
}
private async apiGetCall(url: string): Promise<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 [groups, users];
}
private async getUsers(force: boolean): Promise<UserEntry[]> {
const entries: UserEntry[] = [];
const query = this.createDirectoryQuery(this.syncConfig.userFilter);
const setFilter = this.createCustomSet(this.syncConfig.userFilter);
this.logService.info("Querying users.");
this.allUsers = await this.apiGetMany("users" + (query != null ? "?" + query : ""));
this.allUsers.forEach((user) => {
const entry = this.buildUser(user);
if (entry != null && !this.filterOutResult(setFilter, entry.email)) {
entries.push(entry);
}
});
return Promise.resolve(entries);
}
private buildUser(user: any) {
const entry = new UserEntry();
entry.externalId = user.id;
entry.referenceId = user.id;
entry.deleted = false;
entry.disabled = user.status === 2;
entry.email = user.email;
if (!this.validEmailAddress(entry.email) && user.username != null && user.username !== "") {
if (this.validEmailAddress(user.username)) {
entry.email = user.username;
} else if (this.syncConfig.useEmailPrefixSuffix && this.syncConfig.emailSuffix != null) {
entry.email = user.username + this.syncConfig.emailSuffix;
}
}
if (entry.email != null) {
entry.email = entry.email.trim().toLowerCase();
}
if (!this.validEmailAddress(entry.email)) {
return null;
}
return entry;
}
private async getGroups(
force: boolean,
setFilter: [boolean, Set<string>]
): Promise<GroupEntry[]> {
const entries: GroupEntry[] = [];
const query = this.createDirectoryQuery(this.syncConfig.groupFilter);
this.logService.info("Querying groups.");
const roles = await this.apiGetMany("roles" + (query != null ? "?" + query : ""));
roles.forEach((role) => {
const entry = this.buildGroup(role);
if (entry != null && !this.filterOutResult(setFilter, entry.name)) {
entries.push(entry);
}
});
return Promise.resolve(entries);
}
private buildGroup(group: any) {
const entry = new GroupEntry();
entry.externalId = group.id;
entry.referenceId = group.id;
entry.name = group.name;
if (this.allUsers != null) {
this.allUsers.forEach((user) => {
if (user.role_id != null && user.role_id.indexOf(entry.referenceId) > -1) {
entry.userMemberExternalIds.add(user.id);
}
return 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);
}
return entry;
}
private validEmailAddress(email: string) {
return email != null && email !== '' && ValidEmailRegex.test(email);
private async getAccessToken() {
const response = await fetch(
`https://api.${this.dirConfig.region}.onelogin.com/auth/oauth2/v2/token`,
{
method: "POST",
headers: new Headers({
Authorization:
"Basic " + btoa(this.dirConfig.clientId + ":" + this.dirConfig.clientSecret),
"Content-Type": "application/json; charset=utf-8",
Accept: "application/json",
}),
body: JSON.stringify({
grant_type: "client_credentials",
}),
}
);
if (response.status === 200) {
const responseJson = await response.json();
if (responseJson.access_token != null) {
return responseJson.access_token;
}
}
return null;
}
private async apiGetCall(url: string): Promise<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 { SyncConfiguration } from '../models/syncConfiguration';
import { UserEntry } from '../models/userEntry';
import { GroupEntry } from "../models/groupEntry";
import { SyncConfiguration } from "../models/syncConfiguration";
import { UserEntry } from "../models/userEntry";
import { OrganizationImportRequest } from 'jslib-common/models/request/organizationImportRequest';
import { OrganizationImportRequest } from "jslib-common/models/request/organizationImportRequest";
import { ApiService } from 'jslib-common/abstractions/api.service';
import { CryptoFunctionService } from 'jslib-common/abstractions/cryptoFunction.service';
import { EnvironmentService } from 'jslib-common/abstractions/environment.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { MessagingService } from 'jslib-common/abstractions/messaging.service';
import { ApiService } from "jslib-common/abstractions/api.service";
import { CryptoFunctionService } from "jslib-common/abstractions/cryptoFunction.service";
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { Utils } from 'jslib-common/misc/utils';
import { Utils } from "jslib-common/misc/utils";
import { AzureDirectoryService } from './azure-directory.service';
import { ConfigurationService } from './configuration.service';
import { IDirectoryService } from './directory.service';
import { GSuiteDirectoryService } from './gsuite-directory.service';
import { LdapDirectoryService } from './ldap-directory.service';
import { OktaDirectoryService } from './okta-directory.service';
import { OneLoginDirectoryService } from './onelogin-directory.service';
import { AzureDirectoryService } from "./azure-directory.service";
import { ConfigurationService } from "./configuration.service";
import { IDirectoryService } from "./directory.service";
import { GSuiteDirectoryService } from "./gsuite-directory.service";
import { LdapDirectoryService } from "./ldap-directory.service";
import { OktaDirectoryService } from "./okta-directory.service";
import { OneLoginDirectoryService } from "./onelogin-directory.service";
export class SyncService {
private dirType: DirectoryType;
private dirType: DirectoryType;
constructor(private configurationService: ConfigurationService, private logService: LogService,
private cryptoFunctionService: CryptoFunctionService, private apiService: ApiService,
private messagingService: MessagingService, private i18nService: I18nService,
private environmentService: EnvironmentService) { }
constructor(
private configurationService: ConfigurationService,
private logService: LogService,
private cryptoFunctionService: CryptoFunctionService,
private apiService: ApiService,
private messagingService: MessagingService,
private i18nService: I18nService,
private environmentService: EnvironmentService
) {}
async sync(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> {
this.dirType = await this.configurationService.getDirectoryType();
if (this.dirType == null) {
throw new Error('No directory configured.');
}
const directoryService = this.getDirectoryService();
if (directoryService == null) {
throw new Error('Cannot load directory service.');
}
const syncConfig = await this.configurationService.getSync();
const startingGroupDelta = await this.configurationService.getGroupDeltaToken();
const startingUserDelta = await this.configurationService.getUserDeltaToken();
const now = new Date();
this.messagingService.send('dirSyncStarted');
try {
const entries = await directoryService.getEntries(force || syncConfig.overwriteExisting, test);
let groups = entries[0];
let users = this.filterUnsupportedUsers(entries[1]);
if (groups != null && groups.length > 0) {
this.flattenUsersToGroups(groups, groups);
}
users = this.removeDuplicateUsers(users);
if (test || (!syncConfig.overwriteExisting &&
(groups == null || groups.length === 0) && (users == null || users.length === 0))) {
if (!test) {
await this.saveSyncTimes(syncConfig, now);
}
this.messagingService.send('dirSyncCompleted', { successfully: true });
return [groups, users];
}
const req = this.buildRequest(groups, users, syncConfig.removeDisabled, syncConfig.overwriteExisting, syncConfig.largeImport);
const reqJson = JSON.stringify(req);
const orgId = await this.configurationService.getOrganizationId();
if (orgId == null) {
throw new Error('Organization not set.');
}
// TODO: Remove hashLegacy once we're sure clients have had time to sync new hashes
let hashLegacy: string = null;
const hashBuffLegacy = await this.cryptoFunctionService.hash(this.environmentService.getApiUrl() + reqJson, 'sha256');
if (hashBuffLegacy != null) {
hashLegacy = Utils.fromBufferToB64(hashBuffLegacy);
}
let hash: string = null;
const hashBuff = await this.cryptoFunctionService.hash(this.environmentService.getApiUrl() + orgId + reqJson, 'sha256');
if (hashBuff != null) {
hash = Utils.fromBufferToB64(hashBuff);
}
const lastHash = await this.configurationService.getLastSyncHash();
if (lastHash == null || (hash !== lastHash && hashLegacy !== lastHash)) {
await this.apiService.postPublicImportDirectory(req);
await this.configurationService.saveLastSyncHash(hash);
} else {
groups = null;
users = null;
}
await this.saveSyncTimes(syncConfig, now);
this.messagingService.send('dirSyncCompleted', { successfully: true });
return [groups, users];
} catch (e) {
if (!test) {
await this.configurationService.saveGroupDeltaToken(startingGroupDelta);
await this.configurationService.saveUserDeltaToken(startingUserDelta);
}
this.messagingService.send('dirSyncCompleted', { successfully: false });
throw e;
}
async sync(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> {
this.dirType = await this.configurationService.getDirectoryType();
if (this.dirType == null) {
throw new Error("No directory configured.");
}
private removeDuplicateUsers(users: UserEntry[]) {
const uniqueUsers = new Array<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);
}
return uniqueUsers;
const directoryService = this.getDirectoryService();
if (directoryService == null) {
throw new Error("Cannot load directory service.");
}
private filterUnsupportedUsers(users: UserEntry[]): UserEntry[] {
return users == null ? null : users.filter(u => u.email?.length <= 256);
const syncConfig = await this.configurationService.getSync();
const startingGroupDelta = await this.configurationService.getGroupDeltaToken();
const startingUserDelta = await this.configurationService.getUserDeltaToken();
const now = new Date();
this.messagingService.send("dirSyncStarted");
try {
const entries = await directoryService.getEntries(
force || syncConfig.overwriteExisting,
test
);
let groups = entries[0];
let users = this.filterUnsupportedUsers(entries[1]);
if (groups != null && groups.length > 0) {
this.flattenUsersToGroups(groups, groups);
}
users = this.removeDuplicateUsers(users);
if (
test ||
(!syncConfig.overwriteExisting &&
(groups == null || groups.length === 0) &&
(users == null || users.length === 0))
) {
if (!test) {
await this.saveSyncTimes(syncConfig, now);
}
this.messagingService.send("dirSyncCompleted", { successfully: true });
return [groups, users];
}
const req = this.buildRequest(
groups,
users,
syncConfig.removeDisabled,
syncConfig.overwriteExisting,
syncConfig.largeImport
);
const reqJson = JSON.stringify(req);
const orgId = await this.configurationService.getOrganizationId();
if (orgId == null) {
throw new Error("Organization not set.");
}
// TODO: Remove hashLegacy once we're sure clients have had time to sync new hashes
let hashLegacy: string = null;
const hashBuffLegacy = await this.cryptoFunctionService.hash(
this.environmentService.getApiUrl() + reqJson,
"sha256"
);
if (hashBuffLegacy != null) {
hashLegacy = Utils.fromBufferToB64(hashBuffLegacy);
}
let hash: string = null;
const hashBuff = await this.cryptoFunctionService.hash(
this.environmentService.getApiUrl() + orgId + reqJson,
"sha256"
);
if (hashBuff != null) {
hash = Utils.fromBufferToB64(hashBuff);
}
const lastHash = await this.configurationService.getLastSyncHash();
if (lastHash == null || (hash !== lastHash && hashLegacy !== lastHash)) {
await this.apiService.postPublicImportDirectory(req);
await this.configurationService.saveLastSyncHash(hash);
} else {
groups = null;
users = null;
}
await this.saveSyncTimes(syncConfig, now);
this.messagingService.send("dirSyncCompleted", { successfully: true });
return [groups, users];
} catch (e) {
if (!test) {
await this.configurationService.saveGroupDeltaToken(startingGroupDelta);
await this.configurationService.saveUserDeltaToken(startingUserDelta);
}
this.messagingService.send("dirSyncCompleted", { successfully: false });
throw e;
}
}
private removeDuplicateUsers(users: UserEntry[]) {
const uniqueUsers = new Array<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> {
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;
}
return uniqueUsers;
}
private getDirectoryService(): IDirectoryService {
switch (this.dirType) {
case DirectoryType.GSuite:
return new GSuiteDirectoryService(this.configurationService, this.logService, this.i18nService);
case DirectoryType.AzureActiveDirectory:
return new AzureDirectoryService(this.configurationService, this.logService, this.i18nService);
case DirectoryType.Ldap:
return new LdapDirectoryService(this.configurationService, this.logService, this.i18nService);
case DirectoryType.Okta:
return new OktaDirectoryService(this.configurationService, this.logService, this.i18nService);
case DirectoryType.OneLogin:
return new OneLoginDirectoryService(this.configurationService, this.logService, this.i18nService);
default:
return null;
}
}
private filterUnsupportedUsers(users: UserEntry[]): UserEntry[] {
return users == null ? null : users.filter((u) => u.email?.length <= 256);
}
private buildRequest(groups: GroupEntry[], users: UserEntry[], removeDisabled: boolean, overwriteExisting: boolean,
largeImport: boolean = false) {
return new OrganizationImportRequest({
groups: (groups ?? []).map(g => {
return {
name: g.name,
externalId: g.externalId,
memberExternalIds: Array.from(g.userMemberExternalIds),
};
}),
users: (users ?? []).map(u => {
return {
email: u.email,
externalId: u.externalId,
deleted: u.deleted || (removeDisabled && u.disabled),
};
}),
overwriteExisting: overwriteExisting,
largeImport: largeImport,
});
private flattenUsersToGroups(levelGroups: GroupEntry[], allGroups: GroupEntry[]): Set<string> {
let allUsers = new Set<string>();
if (allGroups == null) {
return allUsers;
}
for (const group of levelGroups) {
const childGroups = allGroups.filter((g) => group.groupMemberReferenceIds.has(g.referenceId));
const childUsers = this.flattenUsersToGroups(childGroups, allGroups);
childUsers.forEach((id) => group.userMemberExternalIds.add(id));
allUsers = new Set([...allUsers, ...group.userMemberExternalIds]);
}
return allUsers;
}
private async saveSyncTimes(syncConfig: SyncConfiguration, time: Date) {
if (syncConfig.groups) {
await this.configurationService.saveLastGroupSyncDate(time);
}
if (syncConfig.users) {
await this.configurationService.saveLastUserSyncDate(time);
}
private getDirectoryService(): IDirectoryService {
switch (this.dirType) {
case DirectoryType.GSuite:
return new GSuiteDirectoryService(
this.configurationService,
this.logService,
this.i18nService
);
case DirectoryType.AzureActiveDirectory:
return new AzureDirectoryService(
this.configurationService,
this.logService,
this.i18nService
);
case DirectoryType.Ldap:
return new LdapDirectoryService(
this.configurationService,
this.logService,
this.i18nService
);
case DirectoryType.Okta:
return new OktaDirectoryService(
this.configurationService,
this.logService,
this.i18nService
);
case DirectoryType.OneLogin:
return new OneLoginDirectoryService(
this.configurationService,
this.logService,
this.i18nService
);
default:
return null;
}
}
private buildRequest(
groups: GroupEntry[],
users: UserEntry[],
removeDisabled: boolean,
overwriteExisting: boolean,
largeImport: boolean = false
) {
return new OrganizationImportRequest({
groups: (groups ?? []).map((g) => {
return {
name: g.name,
externalId: g.externalId,
memberExternalIds: Array.from(g.userMemberExternalIds),
};
}),
users: (users ?? []).map((u) => {
return {
email: u.email,
externalId: u.externalId,
deleted: u.deleted || (removeDisabled && u.disabled),
};
}),
overwriteExisting: overwriteExisting,
largeImport: largeImport,
});
}
private async saveSyncTimes(syncConfig: SyncConfiguration, time: Date) {
if (syncConfig.groups) {
await this.configurationService.saveLastGroupSyncDate(time);
}
if (syncConfig.users) {
await this.configurationService.saveLastUserSyncDate(time);
}
}
}

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

View File

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

View File

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

View File

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

View File

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

View File

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