mirror of
https://github.com/bitwarden/directory-connector
synced 2025-12-05 23:53:21 +00:00
Compare commits
25 Commits
v2025.3.0
...
jmccannon/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
376d4b1c22 | ||
|
|
7450163ff5 | ||
|
|
1deb22a446 | ||
|
|
115a60316d | ||
|
|
e11225b2ce | ||
|
|
4909d306ba | ||
|
|
caa8c4d070 | ||
|
|
ed1d941282 | ||
|
|
f6f874360f | ||
|
|
18b110e70d | ||
|
|
83c42cec73 | ||
|
|
2d80fceb8c | ||
|
|
0489f0cbe9 | ||
|
|
c5d4cb9fb6 | ||
|
|
16d6647090 | ||
|
|
a08673917b | ||
|
|
27e1ab9bcf | ||
|
|
3573e201a6 | ||
|
|
23d285a9f6 | ||
|
|
527d2cb75d | ||
|
|
42efd689e3 | ||
|
|
2fe980dea6 | ||
|
|
9446eedec7 | ||
|
|
41ee0d82d5 | ||
|
|
40a85bb875 |
50
.github/workflows/build.yml
vendored
50
.github/workflows/build.yml
vendored
@@ -48,8 +48,8 @@ jobs:
|
||||
needs: setup
|
||||
env:
|
||||
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
|
||||
_PKG_FETCH_NODE_VERSION: 18.5.0
|
||||
_PKG_FETCH_VERSION: 3.4
|
||||
_PKG_FETCH_NODE_VERSION: 22.13.1
|
||||
_PKG_FETCH_VERSION: 3.5
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
@@ -59,7 +59,7 @@ jobs:
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
node-version: '18'
|
||||
node-version: '22'
|
||||
|
||||
- name: Update NPM
|
||||
run: |
|
||||
@@ -143,8 +143,8 @@ jobs:
|
||||
needs: setup
|
||||
env:
|
||||
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
|
||||
_PKG_FETCH_NODE_VERSION: 18.5.0
|
||||
_PKG_FETCH_VERSION: 3.4
|
||||
_PKG_FETCH_NODE_VERSION: 22.13.1
|
||||
_PKG_FETCH_VERSION: 3.5
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
@@ -154,7 +154,7 @@ jobs:
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
node-version: '18'
|
||||
node-version: '22'
|
||||
|
||||
- name: Update NPM
|
||||
run: |
|
||||
@@ -230,8 +230,8 @@ jobs:
|
||||
needs: setup
|
||||
env:
|
||||
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
|
||||
_WIN_PKG_FETCH_VERSION: 18.5.0
|
||||
_WIN_PKG_VERSION: 3.4
|
||||
_WIN_PKG_FETCH_VERSION: 22.13.1
|
||||
_WIN_PKG_VERSION: 3.5
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
@@ -246,7 +246,7 @@ jobs:
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
node-version: '18'
|
||||
node-version: '22'
|
||||
|
||||
- name: Update NPM
|
||||
run: |
|
||||
@@ -386,7 +386,7 @@ jobs:
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
node-version: '18'
|
||||
node-version: '22'
|
||||
|
||||
- name: Update NPM
|
||||
run: |
|
||||
@@ -404,15 +404,31 @@ jobs:
|
||||
- name: Install Node dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Login to Azure
|
||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "code-signing-vault-url,
|
||||
code-signing-client-id,
|
||||
code-signing-tenant-id,
|
||||
code-signing-client-secret,
|
||||
code-signing-cert-name"
|
||||
|
||||
- name: Build & Sign
|
||||
run: npm run dist:win
|
||||
env:
|
||||
ELECTRON_BUILDER_SIGN: 1
|
||||
SIGNING_VAULT_URL: ${{ secrets.SIGNING_VAULT_URL }}
|
||||
SIGNING_CLIENT_ID: ${{ secrets.SIGNING_CLIENT_ID }}
|
||||
SIGNING_TENANT_ID: ${{ secrets.SIGNING_TENANT_ID }}
|
||||
SIGNING_CLIENT_SECRET: ${{ secrets.SIGNING_CLIENT_SECRET }}
|
||||
SIGNING_CERT_NAME: ${{ secrets.SIGNING_CERT_NAME }}
|
||||
SIGNING_VAULT_URL: ${{ steps.retrieve-secrets.outputs.code-signing-vault-url }}
|
||||
SIGNING_CLIENT_ID: ${{ steps.retrieve-secrets.outputs.code-signing-client-id }}
|
||||
SIGNING_TENANT_ID: ${{ steps.retrieve-secrets.outputs.code-signing-tenant-id }}
|
||||
SIGNING_CLIENT_SECRET: ${{ steps.retrieve-secrets.outputs.code-signing-client-secret }}
|
||||
SIGNING_CERT_NAME: ${{ steps.retrieve-secrets.outputs.code-signing-cert-name }}
|
||||
|
||||
- name: Upload Portable Executable to GitHub
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
@@ -460,7 +476,7 @@ jobs:
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
node-version: '18'
|
||||
node-version: '22'
|
||||
|
||||
- name: Update NPM
|
||||
run: |
|
||||
@@ -514,7 +530,7 @@ jobs:
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
node-version: '18'
|
||||
node-version: '22'
|
||||
|
||||
- name: Update NPM
|
||||
run: |
|
||||
|
||||
2
.github/workflows/integration-test.yml
vendored
2
.github/workflows/integration-test.yml
vendored
@@ -74,5 +74,3 @@ jobs:
|
||||
|
||||
- name: Upload results to codecov.io
|
||||
uses: codecov/test-results-action@4e79e65778be1cecd5df25e14af1eafb6df80ea9 # v1.0.2
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -64,5 +64,3 @@ jobs:
|
||||
|
||||
- name: Upload results to codecov.io
|
||||
uses: codecov/test-results-action@4e79e65778be1cecd5df25e14af1eafb6df80ea9 # v1.0.2
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
@@ -8,16 +8,12 @@ export class OrganizationImportRequest {
|
||||
overwriteExisting = false;
|
||||
largeImport = false;
|
||||
|
||||
constructor(
|
||||
model:
|
||||
| {
|
||||
constructor(model: {
|
||||
groups: Required<OrganizationImportGroupRequest>[];
|
||||
users: Required<OrganizationImportMemberRequest>[];
|
||||
overwriteExisting: boolean;
|
||||
largeImport: boolean;
|
||||
}
|
||||
| ImportDirectoryRequest,
|
||||
) {
|
||||
}) {
|
||||
if (model instanceof ImportDirectoryRequest) {
|
||||
this.groups = model.groups.map((g) => new OrganizationImportGroupRequest(g));
|
||||
this.members = model.users.map((u) => new OrganizationImportMemberRequest(u));
|
||||
|
||||
1584
package-lock.json
generated
1584
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
33
package.json
33
package.json
@@ -2,7 +2,7 @@
|
||||
"name": "@bitwarden/directory-connector",
|
||||
"productName": "Bitwarden Directory Connector",
|
||||
"description": "Sync your user directory to your Bitwarden organization.",
|
||||
"version": "2025.3.0",
|
||||
"version": "2025.5.0",
|
||||
"keywords": [
|
||||
"bitwarden",
|
||||
"password",
|
||||
@@ -73,7 +73,7 @@
|
||||
"test:types": "npx tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "17.3.11",
|
||||
"@angular-devkit/build-angular": "17.3.17",
|
||||
"@angular-eslint/eslint-plugin-template": "17.5.3",
|
||||
"@angular-eslint/template-parser": "17.5.3",
|
||||
"@angular/compiler-cli": "17.3.12",
|
||||
@@ -81,7 +81,7 @@
|
||||
"@electron/rebuild": "3.7.1",
|
||||
"@fluffy-spoon/substitute": "1.208.0",
|
||||
"@microsoft/microsoft-graph-types": "2.40.0",
|
||||
"@ngtools/webpack": "17.3.11",
|
||||
"@ngtools/webpack": "17.3.17",
|
||||
"@types/inquirer": "8.2.10",
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/lowdb": "1.0.15",
|
||||
@@ -90,22 +90,22 @@
|
||||
"@types/node-forge": "1.3.11",
|
||||
"@types/proper-lockfile": "4.1.4",
|
||||
"@types/tldjs": "2.3.4",
|
||||
"@typescript-eslint/eslint-plugin": "8.23.0",
|
||||
"@typescript-eslint/parser": "8.23.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.32.1",
|
||||
"@typescript-eslint/parser": "8.32.1",
|
||||
"clean-webpack-plugin": "4.0.0",
|
||||
"concurrently": "9.1.2",
|
||||
"copy-webpack-plugin": "12.0.2",
|
||||
"cross-env": "7.0.3",
|
||||
"css-loader": "7.1.2",
|
||||
"dotenv": "16.4.7",
|
||||
"dotenv": "16.5.0",
|
||||
"electron": "34.1.1",
|
||||
"electron-builder": "24.13.3",
|
||||
"electron-log": "5.2.4",
|
||||
"electron-reload": "2.0.0-alpha.1",
|
||||
"electron-store": "8.2.0",
|
||||
"electron-updater": "6.3.9",
|
||||
"electron-updater": "6.6.2",
|
||||
"eslint": "8.57.1",
|
||||
"eslint-config-prettier": "10.0.1",
|
||||
"eslint-config-prettier": "10.1.5",
|
||||
"eslint-import-resolver-typescript": "3.7.0",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"eslint-plugin-rxjs": "5.0.3",
|
||||
@@ -117,23 +117,22 @@
|
||||
"jest": "29.7.0",
|
||||
"jest-junit": "16.0.0",
|
||||
"jest-mock-extended": "3.0.7",
|
||||
"jest-preset-angular": "14.5.0",
|
||||
"lint-staged": "15.4.1",
|
||||
"jest-preset-angular": "14.5.5",
|
||||
"lint-staged": "15.5.2",
|
||||
"mini-css-extract-plugin": "2.9.2",
|
||||
"minimatch": "3.1.2",
|
||||
"node-abi": "3.74.0",
|
||||
"node-forge": "1.3.1",
|
||||
"node-loader": "2.1.0",
|
||||
"pkg": "5.8.1",
|
||||
"prettier": "3.4.2",
|
||||
"prettier": "3.5.3",
|
||||
"rimraf": "6.0.1",
|
||||
"rxjs": "7.8.1",
|
||||
"rxjs": "7.8.2",
|
||||
"sass": "1.79.4",
|
||||
"sass-loader": "16.0.4",
|
||||
"ts-jest": "29.2.5",
|
||||
"ts-loader": "9.5.2",
|
||||
"tsconfig-paths-webpack-plugin": "4.2.0",
|
||||
"type-fest": "4.32.0",
|
||||
"type-fest": "4.41.0",
|
||||
"typescript": "5.4.5",
|
||||
"webpack": "5.97.1",
|
||||
"webpack-cli": "6.0.1",
|
||||
@@ -157,19 +156,19 @@
|
||||
"browser-hrtime": "1.1.8",
|
||||
"chalk": "4.1.2",
|
||||
"commander": "13.1.0",
|
||||
"core-js": "3.40.0",
|
||||
"core-js": "3.42.0",
|
||||
"form-data": "4.0.1",
|
||||
"google-auth-library": "9.15.1",
|
||||
"googleapis": "144.0.0",
|
||||
"https-proxy-agent": "7.0.6",
|
||||
"inquirer": "8.2.6",
|
||||
"keytar": "7.9.0",
|
||||
"ldapts": "7.3.1",
|
||||
"ldapts": "7.4.0",
|
||||
"lowdb": "1.0.0",
|
||||
"ngx-toastr": "19.0.0",
|
||||
"node-fetch": "2.7.0",
|
||||
"proper-lockfile": "4.1.2",
|
||||
"rxjs": "7.8.1",
|
||||
"rxjs": "7.8.2",
|
||||
"tldjs": "2.3.1",
|
||||
"zone.js": "0.14.10"
|
||||
},
|
||||
|
||||
@@ -3,11 +3,15 @@ import { OrganizationImportRequest } from "@/jslib/common/src/models/request/org
|
||||
import { GroupEntry } from "@/src/models/groupEntry";
|
||||
import { UserEntry } from "@/src/models/userEntry";
|
||||
|
||||
export interface RequestBuilderOptions {
|
||||
removeDisabled: boolean;
|
||||
overwriteExisting: boolean;
|
||||
}
|
||||
|
||||
export abstract class RequestBuilder {
|
||||
buildRequest: (
|
||||
groups: GroupEntry[],
|
||||
users: UserEntry[],
|
||||
removeDisabled: boolean,
|
||||
overwriteExisting: boolean,
|
||||
options: RequestBuilderOptions,
|
||||
) => OrganizationImportRequest[];
|
||||
}
|
||||
|
||||
@@ -22,18 +22,15 @@
|
||||
class="btn btn-primary"
|
||||
[disabled]="startForm.loading"
|
||||
>
|
||||
<i class="bwi bwi-play bwi-fw" [hidden]="startForm.loading"></i>
|
||||
<i class="bwi bwi-spinner bwi-fw bwi-spin" [hidden]="!startForm.loading"></i>
|
||||
{{ "startSync" | i18n }}
|
||||
</button>
|
||||
</form>
|
||||
<button type="button" (click)="stop()" class="btn btn-primary">
|
||||
<i class="bwi bwi-stop bwi-fw"></i>
|
||||
<button type="button" (click)="stop()" class="btn btn-danger text-white">
|
||||
{{ "stopSync" | i18n }}
|
||||
</button>
|
||||
<form #syncForm [appApiAction]="syncPromise" class="d-inline">
|
||||
<button type="button" (click)="sync()" class="btn btn-primary" [disabled]="syncForm.loading">
|
||||
<i class="bwi bwi-refresh bwi-fw" [ngClass]="{ 'bwi-spin': syncForm.loading }"></i>
|
||||
{{ "syncNow" | i18n }}
|
||||
</button>
|
||||
</form>
|
||||
@@ -51,7 +48,6 @@
|
||||
[disabled]="simForm.loading"
|
||||
>
|
||||
<i class="bwi bwi-spinner bwi-fw bwi-spin" [hidden]="!simForm.loading"></i>
|
||||
<i class="bwi bwi-bug bwi-fw" [hidden]="simForm.loading"></i>
|
||||
{{ "testNow" | i18n }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@@ -614,7 +614,7 @@
|
||||
{{ "ex" | i18n }} exclude:joe@company.com | profile.firstName eq "John"
|
||||
</div>
|
||||
<div class="form-text" *ngIf="directory === directoryType.GSuite">
|
||||
{{ "ex" | i18n }} exclude:joe@company.com | orgName=Engineering
|
||||
{{ "ex" | i18n }} exclude:joe@company.com | orgUnitPath=/Engineering
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3" [hidden]="directory != directoryType.Ldap">
|
||||
|
||||
@@ -2,19 +2,16 @@
|
||||
<ul class="nav nav-tabs mb-3">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" routerLink="dashboard" routerLinkActive="active">
|
||||
<i class="bwi bwi-dashboard"></i>
|
||||
{{ "dashboard" | i18n }}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" routerLink="settings" routerLinkActive="active">
|
||||
<i class="bwi bwi-cogs"></i>
|
||||
{{ "settings" | i18n }}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" routerLink="more" routerLinkActive="active">
|
||||
<i class="bwi bwi-sliders"></i>
|
||||
{{ "more" | i18n }}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
5
src/scss/bootstrap.scss
vendored
5
src/scss/bootstrap.scss
vendored
@@ -8,8 +8,9 @@ $theme-colors: (
|
||||
"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";
|
||||
$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;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { OrganizationImportRequest } from "@/jslib/common/src/models/request/org
|
||||
import { GroupEntry } from "@/src/models/groupEntry";
|
||||
import { UserEntry } from "@/src/models/userEntry";
|
||||
|
||||
import { RequestBuilder } from "../abstractions/request-builder.service";
|
||||
import { RequestBuilder, RequestBuilderOptions } from "../abstractions/request-builder.service";
|
||||
|
||||
import { batchSize } from "./sync.service";
|
||||
|
||||
@@ -16,17 +16,22 @@ export class BatchRequestBuilder implements RequestBuilder {
|
||||
buildRequest(
|
||||
groups: GroupEntry[],
|
||||
users: UserEntry[],
|
||||
removeDisabled: boolean,
|
||||
overwriteExisting: boolean,
|
||||
options: RequestBuilderOptions,
|
||||
): OrganizationImportRequest[] {
|
||||
if (options.overwriteExisting) {
|
||||
throw new Error(
|
||||
"You cannot use the 'Remove and re-add organization users during the next sync' option with large imports.",
|
||||
);
|
||||
}
|
||||
|
||||
const requests: OrganizationImportRequest[] = [];
|
||||
|
||||
if (users.length > 0) {
|
||||
if (users?.length > 0) {
|
||||
const usersRequest = users.map((u) => {
|
||||
return {
|
||||
email: u.email,
|
||||
externalId: u.externalId,
|
||||
deleted: u.deleted || (removeDisabled && u.disabled),
|
||||
deleted: u.deleted || (options.removeDisabled && u.disabled),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -37,13 +42,13 @@ export class BatchRequestBuilder implements RequestBuilder {
|
||||
groups: [],
|
||||
users: u,
|
||||
largeImport: true,
|
||||
overwriteExisting,
|
||||
overwriteExisting: false,
|
||||
});
|
||||
requests.push(req);
|
||||
}
|
||||
}
|
||||
|
||||
if (groups.length > 0) {
|
||||
if (groups?.length > 0) {
|
||||
const groupRequest = groups.map((g) => {
|
||||
return {
|
||||
name: g.name,
|
||||
@@ -59,7 +64,7 @@ export class BatchRequestBuilder implements RequestBuilder {
|
||||
groups: g,
|
||||
users: [],
|
||||
largeImport: true,
|
||||
overwriteExisting,
|
||||
overwriteExisting: false,
|
||||
});
|
||||
requests.push(req);
|
||||
}
|
||||
|
||||
@@ -1,46 +1,74 @@
|
||||
import { GroupEntry } from "@/src/models/groupEntry";
|
||||
import { GetUniqueString } from "@/jslib/common/spec/utils";
|
||||
|
||||
import { UserEntry } from "@/src/models/userEntry";
|
||||
|
||||
import { RequestBuilderOptions } from "../abstractions/request-builder.service";
|
||||
import { groupSimulator, userSimulator } from "../utils/request-builder-helper";
|
||||
|
||||
import { BatchRequestBuilder } from "./batch-request-builder";
|
||||
import { SingleRequestBuilder } from "./single-request-builder";
|
||||
|
||||
describe("BatchRequestBuilder", () => {
|
||||
let batchRequestBuilder: BatchRequestBuilder;
|
||||
let singleRequestBuilder: SingleRequestBuilder;
|
||||
|
||||
function userSimulator(userCount: number) {
|
||||
return Array(userCount).fill(new UserEntry());
|
||||
}
|
||||
|
||||
function groupSimulator(groupCount: number) {
|
||||
return Array(groupCount).fill(new GroupEntry());
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
batchRequestBuilder = new BatchRequestBuilder();
|
||||
singleRequestBuilder = new SingleRequestBuilder();
|
||||
});
|
||||
|
||||
const defaultOptions: RequestBuilderOptions = Object.freeze({
|
||||
overwriteExisting: false,
|
||||
removeDisabled: false,
|
||||
});
|
||||
|
||||
it("BatchRequestBuilder batches requests for > 2000 users", () => {
|
||||
const mockGroups = groupSimulator(11000);
|
||||
const mockUsers = userSimulator(11000);
|
||||
|
||||
const requests = batchRequestBuilder.buildRequest(mockGroups, mockUsers, true, true);
|
||||
const requests = batchRequestBuilder.buildRequest(mockGroups, mockUsers, defaultOptions);
|
||||
|
||||
expect(requests.length).toEqual(12);
|
||||
});
|
||||
|
||||
it("SingleRequestBuilder returns single request for 200 users", () => {
|
||||
const mockGroups = groupSimulator(200);
|
||||
const mockUsers = userSimulator(200);
|
||||
it("BatchRequestBuilder throws error when overwriteExisting is true", () => {
|
||||
const mockGroups = groupSimulator(11000);
|
||||
const mockUsers = userSimulator(11000);
|
||||
const options = { ...defaultOptions, overwriteExisting: true };
|
||||
|
||||
const requests = singleRequestBuilder.buildRequest(mockGroups, mockUsers, true, true);
|
||||
const r = () => batchRequestBuilder.buildRequest(mockGroups, mockUsers, options);
|
||||
|
||||
expect(requests.length).toEqual(1);
|
||||
expect(r).toThrow(
|
||||
"You cannot use the 'Remove and re-add organization users during the next sync' option with large imports.",
|
||||
);
|
||||
});
|
||||
|
||||
it("BatchRequestBuilder returns requests with deleted users when removeDisabled is true", () => {
|
||||
const mockGroups = groupSimulator(11000);
|
||||
const mockUsers = userSimulator(11000);
|
||||
|
||||
const disabledUser1 = new UserEntry();
|
||||
const disabledUserEmail1 = GetUniqueString() + "@email.com";
|
||||
|
||||
const disabledUser2 = new UserEntry();
|
||||
const disabledUserEmail2 = GetUniqueString() + "@email.com";
|
||||
|
||||
disabledUser1.disabled = true;
|
||||
disabledUser1.email = disabledUserEmail1;
|
||||
disabledUser2.disabled = true;
|
||||
disabledUser2.email = disabledUserEmail2;
|
||||
|
||||
mockUsers[0] = disabledUser1;
|
||||
mockUsers.push(disabledUser2);
|
||||
|
||||
const options = { ...defaultOptions, removeDisabled: true };
|
||||
const requests = batchRequestBuilder.buildRequest(mockGroups, mockUsers, options);
|
||||
|
||||
expect(requests[0].members).toContainEqual({ email: disabledUserEmail1, deleted: true });
|
||||
expect(requests[1].members.find((m) => m.deleted)).toBeUndefined();
|
||||
expect(requests[3].members.find((m) => m.deleted)).toBeUndefined();
|
||||
expect(requests[4].members.find((m) => m.deleted)).toBeUndefined();
|
||||
expect(requests[5].members).toContainEqual({ email: disabledUserEmail2, deleted: true });
|
||||
});
|
||||
|
||||
it("BatchRequestBuilder retuns an empty array when there are no users or groups", () => {
|
||||
const requests = batchRequestBuilder.buildRequest([], [], true, true);
|
||||
const requests = batchRequestBuilder.buildRequest([], [], defaultOptions);
|
||||
|
||||
expect(requests).toEqual([]);
|
||||
});
|
||||
|
||||
79
src/services/single-request-builder.spec.ts
Normal file
79
src/services/single-request-builder.spec.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { GetUniqueString } from "@/jslib/common/spec/utils";
|
||||
|
||||
import { UserEntry } from "@/src/models/userEntry";
|
||||
|
||||
import { RequestBuilderOptions } from "../abstractions/request-builder.service";
|
||||
import { groupSimulator, userSimulator } from "../utils/request-builder-helper";
|
||||
|
||||
import { SingleRequestBuilder } from "./single-request-builder";
|
||||
|
||||
describe("SingleRequestBuilder", () => {
|
||||
let singleRequestBuilder: SingleRequestBuilder;
|
||||
|
||||
beforeEach(async () => {
|
||||
singleRequestBuilder = new SingleRequestBuilder();
|
||||
});
|
||||
|
||||
const defaultOptions: RequestBuilderOptions = Object.freeze({
|
||||
overwriteExisting: false,
|
||||
removeDisabled: false,
|
||||
});
|
||||
|
||||
it("SingleRequestBuilder returns single request for 200 users", () => {
|
||||
const mockGroups = groupSimulator(200);
|
||||
const mockUsers = userSimulator(200);
|
||||
|
||||
const requests = singleRequestBuilder.buildRequest(mockGroups, mockUsers, defaultOptions);
|
||||
|
||||
expect(requests.length).toEqual(1);
|
||||
});
|
||||
|
||||
it("SingleRequestBuilder returns request with overwriteExisting enabled", () => {
|
||||
const mockGroups = groupSimulator(200);
|
||||
const mockUsers = userSimulator(200);
|
||||
|
||||
const options = { ...defaultOptions, overwriteExisting: true };
|
||||
const request = singleRequestBuilder.buildRequest(mockGroups, mockUsers, options)[0];
|
||||
|
||||
expect(request.overwriteExisting).toBe(true);
|
||||
});
|
||||
|
||||
it("SingleRequestBuilder returns request with deleted user when removeDisabled is true", () => {
|
||||
const mockGroups = groupSimulator(200);
|
||||
const mockUsers = userSimulator(200);
|
||||
|
||||
const disabledUser = new UserEntry();
|
||||
const disabledUserEmail = GetUniqueString() + "@example.com";
|
||||
disabledUser.disabled = true;
|
||||
disabledUser.email = disabledUserEmail;
|
||||
mockUsers.push(disabledUser);
|
||||
|
||||
const options = { ...defaultOptions, removeDisabled: true };
|
||||
const request = singleRequestBuilder.buildRequest(mockGroups, mockUsers, options)[0];
|
||||
|
||||
expect(request.members.length).toEqual(201);
|
||||
expect(request.members.pop()).toEqual(
|
||||
expect.objectContaining({ email: disabledUserEmail, deleted: true }),
|
||||
);
|
||||
expect(request.overwriteExisting).toBe(false);
|
||||
});
|
||||
|
||||
it("SingleRequestBuilder returns request with deleted user and overwriteExisting enabled when overwriteExisting and removeDisabled are true", () => {
|
||||
const mockGroups = groupSimulator(200);
|
||||
const mockUsers = userSimulator(200);
|
||||
|
||||
const disabledUser = new UserEntry();
|
||||
const disabledUserEmail = GetUniqueString() + "@example.com";
|
||||
disabledUser.disabled = true;
|
||||
disabledUser.email = disabledUserEmail;
|
||||
mockUsers.push(disabledUser);
|
||||
|
||||
const options = { overwriteExisting: true, removeDisabled: true };
|
||||
const request = singleRequestBuilder.buildRequest(mockGroups, mockUsers, options)[0];
|
||||
|
||||
expect(request.members.pop()).toEqual(
|
||||
expect.objectContaining({ email: disabledUserEmail, deleted: true }),
|
||||
);
|
||||
expect(request.overwriteExisting).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -3,7 +3,7 @@ import { OrganizationImportRequest } from "@/jslib/common/src/models/request/org
|
||||
import { GroupEntry } from "@/src/models/groupEntry";
|
||||
import { UserEntry } from "@/src/models/userEntry";
|
||||
|
||||
import { RequestBuilder } from "../abstractions/request-builder.service";
|
||||
import { RequestBuilder, RequestBuilderOptions } from "../abstractions/request-builder.service";
|
||||
|
||||
/**
|
||||
* This class is responsible for building small (<2k users) syncs as a single
|
||||
@@ -15,8 +15,7 @@ export class SingleRequestBuilder implements RequestBuilder {
|
||||
buildRequest(
|
||||
groups: GroupEntry[],
|
||||
users: UserEntry[],
|
||||
removeDisabled: boolean,
|
||||
overwriteExisting: boolean,
|
||||
options: RequestBuilderOptions,
|
||||
): OrganizationImportRequest[] {
|
||||
return [
|
||||
new OrganizationImportRequest({
|
||||
@@ -31,10 +30,10 @@ export class SingleRequestBuilder implements RequestBuilder {
|
||||
return {
|
||||
email: u.email,
|
||||
externalId: u.externalId,
|
||||
deleted: u.deleted || (removeDisabled && u.disabled),
|
||||
deleted: u.deleted || (options.removeDisabled && u.disabled),
|
||||
};
|
||||
}),
|
||||
overwriteExisting: overwriteExisting,
|
||||
overwriteExisting: options.overwriteExisting,
|
||||
largeImport: false,
|
||||
}),
|
||||
];
|
||||
|
||||
132
src/services/sync.service.integration.spec.ts
Normal file
132
src/services/sync.service.integration.spec.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { ApiService } from "@/jslib/common/src/abstractions/api.service";
|
||||
import { CryptoFunctionService } from "@/jslib/common/src/abstractions/cryptoFunction.service";
|
||||
import { MessagingService } from "@/jslib/common/src/abstractions/messaging.service";
|
||||
import { EnvironmentService } from "@/jslib/common/src/services/environment.service";
|
||||
|
||||
import { I18nService } from "../../jslib/common/src/abstractions/i18n.service";
|
||||
import { LogService } from "../../jslib/common/src/abstractions/log.service";
|
||||
import { groupFixtures } from "../../openldap/group-fixtures";
|
||||
import { userFixtures } from "../../openldap/user-fixtures";
|
||||
import { DirectoryFactoryService } from "../abstractions/directory-factory.service";
|
||||
import { DirectoryType } from "../enums/directoryType";
|
||||
import { getLdapConfiguration, getSyncConfiguration } from "../utils/test-fixtures";
|
||||
|
||||
import { BatchRequestBuilder } from "./batch-request-builder";
|
||||
import { LdapDirectoryService } from "./ldap-directory.service";
|
||||
import { SingleRequestBuilder } from "./single-request-builder";
|
||||
import { StateService } from "./state.service";
|
||||
import { SyncService } from "./sync.service";
|
||||
import * as constants from "./sync.service";
|
||||
|
||||
describe("SyncService", () => {
|
||||
let logService: MockProxy<LogService>;
|
||||
let i18nService: MockProxy<I18nService>;
|
||||
let stateService: MockProxy<StateService>;
|
||||
let cryptoFunctionService: MockProxy<CryptoFunctionService>;
|
||||
let apiService: MockProxy<ApiService>;
|
||||
let messagingService: MockProxy<MessagingService>;
|
||||
let environmentService: MockProxy<EnvironmentService>;
|
||||
let directoryFactory: MockProxy<DirectoryFactoryService>;
|
||||
|
||||
let batchRequestBuilder: BatchRequestBuilder;
|
||||
let singleRequestBuilder: SingleRequestBuilder;
|
||||
let syncService: SyncService;
|
||||
let directoryService: LdapDirectoryService;
|
||||
|
||||
const originalBatchSize = constants.batchSize;
|
||||
|
||||
beforeEach(() => {
|
||||
logService = mock();
|
||||
i18nService = mock();
|
||||
stateService = mock();
|
||||
cryptoFunctionService = mock();
|
||||
apiService = mock();
|
||||
messagingService = mock();
|
||||
environmentService = mock();
|
||||
directoryFactory = mock();
|
||||
|
||||
stateService.getDirectoryType.mockResolvedValue(DirectoryType.Ldap);
|
||||
stateService.getOrganizationId.mockResolvedValue("fakeId");
|
||||
|
||||
directoryService = new LdapDirectoryService(logService, i18nService, stateService);
|
||||
directoryFactory.createService.mockReturnValue(directoryService);
|
||||
|
||||
batchRequestBuilder = new BatchRequestBuilder();
|
||||
singleRequestBuilder = new SingleRequestBuilder();
|
||||
|
||||
syncService = new SyncService(
|
||||
cryptoFunctionService,
|
||||
apiService,
|
||||
messagingService,
|
||||
i18nService,
|
||||
environmentService,
|
||||
stateService,
|
||||
batchRequestBuilder,
|
||||
singleRequestBuilder,
|
||||
directoryFactory,
|
||||
);
|
||||
});
|
||||
|
||||
describe("OpenLdap integration: ", () => {
|
||||
it("with largeImport disabled matches directory fixture data", async () => {
|
||||
stateService.getDirectory
|
||||
.calledWith(DirectoryType.Ldap)
|
||||
.mockResolvedValue(getLdapConfiguration());
|
||||
stateService.getSync.mockResolvedValue(
|
||||
getSyncConfiguration({
|
||||
users: true,
|
||||
groups: true,
|
||||
largeImport: false,
|
||||
overwriteExisting: false,
|
||||
}),
|
||||
);
|
||||
|
||||
cryptoFunctionService.hash.mockResolvedValue(new ArrayBuffer(1));
|
||||
// This arranges the last hash to be differet from the ArrayBuffer after it is converted to b64
|
||||
stateService.getLastSyncHash.mockResolvedValue("unique hash");
|
||||
|
||||
const syncResult = await syncService.sync(false, false);
|
||||
|
||||
expect(syncResult).toEqual([groupFixtures, userFixtures]);
|
||||
|
||||
expect(apiService.postPublicImportDirectory).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ overwriteExisting: false }),
|
||||
);
|
||||
expect(apiService.postPublicImportDirectory).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("with largeImport enabled matches directory fixture data", async () => {
|
||||
stateService.getDirectory
|
||||
.calledWith(DirectoryType.Ldap)
|
||||
.mockResolvedValue(getLdapConfiguration());
|
||||
stateService.getSync.mockResolvedValue(
|
||||
getSyncConfiguration({
|
||||
users: true,
|
||||
groups: true,
|
||||
largeImport: true,
|
||||
overwriteExisting: false,
|
||||
}),
|
||||
);
|
||||
|
||||
cryptoFunctionService.hash.mockResolvedValue(new ArrayBuffer(1));
|
||||
// This arranges the last hash to be differet from the ArrayBuffer after it is converted to b64
|
||||
stateService.getLastSyncHash.mockResolvedValue("unique hash");
|
||||
|
||||
// @ts-expect-error This is a workaround to make the batchsize smaller to trigger the batching logic since its a const.
|
||||
constants.batchSize = 4;
|
||||
|
||||
const syncResult = await syncService.sync(false, false);
|
||||
|
||||
expect(syncResult).toEqual([groupFixtures, userFixtures]);
|
||||
expect(apiService.postPublicImportDirectory).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ overwriteExisting: false }),
|
||||
);
|
||||
expect(apiService.postPublicImportDirectory).toHaveBeenCalledTimes(6);
|
||||
|
||||
// @ts-expect-error Reset batch size to original state.
|
||||
constants.batchSize = originalBatchSize;
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -34,6 +34,8 @@ describe("SyncService", () => {
|
||||
|
||||
let syncService: SyncService;
|
||||
|
||||
const originalBatchSize = constants.batchSize;
|
||||
|
||||
beforeEach(() => {
|
||||
cryptoFunctionService = mock();
|
||||
apiService = mock();
|
||||
@@ -115,11 +117,12 @@ describe("SyncService", () => {
|
||||
expect(apiService.postPublicImportDirectory).toHaveBeenCalledWith(mockRequests[3]);
|
||||
expect(apiService.postPublicImportDirectory).toHaveBeenCalledWith(mockRequests[4]);
|
||||
expect(apiService.postPublicImportDirectory).toHaveBeenCalledWith(mockRequests[5]);
|
||||
|
||||
// @ts-expect-error Reset batch size back to original value.
|
||||
constants.batchSize = originalBatchSize;
|
||||
});
|
||||
|
||||
it("does not post for the same hash", async () => {
|
||||
// @ts-expect-error this sets the batch size back to its expexted value for this test.
|
||||
constants.batchSize = 2000;
|
||||
stateService.getSync.mockResolvedValue(getSyncConfiguration({ groups: true, users: true }));
|
||||
cryptoFunctionService.hash.mockResolvedValue(new ArrayBuffer(1));
|
||||
// This arranges the last hash to be the same as the ArrayBuffer after it is converted to b64
|
||||
|
||||
@@ -83,13 +83,7 @@ export class SyncService {
|
||||
return [groups, users];
|
||||
}
|
||||
|
||||
const reqs = this.buildRequest(
|
||||
groups,
|
||||
users,
|
||||
syncConfig.removeDisabled,
|
||||
syncConfig.overwriteExisting,
|
||||
syncConfig.largeImport,
|
||||
);
|
||||
const reqs = this.buildRequest(groups, users, syncConfig);
|
||||
|
||||
const result: HashResult = await this.generateHash(reqs);
|
||||
|
||||
@@ -219,24 +213,12 @@ export class SyncService {
|
||||
private buildRequest(
|
||||
groups: GroupEntry[],
|
||||
users: UserEntry[],
|
||||
removeDisabled: boolean,
|
||||
overwriteExisting: boolean,
|
||||
largeImport = false,
|
||||
syncConfig: SyncConfiguration,
|
||||
): OrganizationImportRequest[] {
|
||||
if (largeImport && groups.length + users.length > batchSize) {
|
||||
return this.batchRequestBuilder.buildRequest(
|
||||
groups,
|
||||
users,
|
||||
overwriteExisting,
|
||||
removeDisabled,
|
||||
);
|
||||
if (syncConfig.largeImport && (groups?.length ?? 0) + (users?.length ?? 0) > batchSize) {
|
||||
return this.batchRequestBuilder.buildRequest(groups, users, syncConfig);
|
||||
} else {
|
||||
return this.singleRequestBuilder.buildRequest(
|
||||
groups,
|
||||
users,
|
||||
overwriteExisting,
|
||||
removeDisabled,
|
||||
);
|
||||
return this.singleRequestBuilder.buildRequest(groups, users, syncConfig);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
26
src/utils/request-builder-helper.ts
Normal file
26
src/utils/request-builder-helper.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { GetUniqueString } from "@/jslib/common/spec/utils";
|
||||
|
||||
import { GroupEntry } from "../models/groupEntry";
|
||||
import { UserEntry } from "../models/userEntry";
|
||||
|
||||
export function userSimulator(userCount: number): UserEntry[] {
|
||||
const users: UserEntry[] = [];
|
||||
while (userCount > 0) {
|
||||
const userEntry = new UserEntry();
|
||||
userEntry.email = GetUniqueString() + "@example.com";
|
||||
users.push(userEntry);
|
||||
userCount--;
|
||||
}
|
||||
return users;
|
||||
}
|
||||
|
||||
export function groupSimulator(groupCount: number): GroupEntry[] {
|
||||
const groups: GroupEntry[] = [];
|
||||
while (groupCount > 0) {
|
||||
const groupEntry = new GroupEntry();
|
||||
groupEntry.name = GetUniqueString();
|
||||
groups.push(groupEntry);
|
||||
groupCount--;
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
Reference in New Issue
Block a user