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

Compare commits

..

25 Commits

Author SHA1 Message Date
jrmccannon
376d4b1c22 upping pkg fetch.
linting fixes.
2025-05-23 15:09:58 -05:00
jrmccannon
7450163ff5 upping the node version to 22 2025-05-23 14:56:16 -05:00
renovate[bot]
1deb22a446 [deps]: Update eslint-config-prettier to v10.1.5 (#753)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-23 11:29:23 -05:00
renovate[bot]
115a60316d [deps]: Update lint-staged to v15.5.2 (#757)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-23 11:11:09 -05:00
renovate[bot]
e11225b2ce [deps]: Update typescript-eslint monorepo to v8.32.1 (#761)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-22 14:51:42 -05:00
renovate[bot]
4909d306ba [deps]: Update dotenv to v16.5.0 (#749)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-22 10:53:01 -05:00
renovate[bot]
caa8c4d070 [deps]: Update core-js to v3.42.0 (#748)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-21 09:36:15 -04:00
Brandon Treston
ed1d941282 remove dependency (#783) 2025-05-21 09:31:45 -04:00
renovate[bot]
f6f874360f [deps]: Update electron-updater to v6.6.2 (#752)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-19 16:54:54 -04:00
renovate[bot]
18b110e70d [deps]: Update ldapts to v7.4.0 (#756)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-19 12:54:17 -04:00
renovate[bot]
83c42cec73 [deps]: Update type-fest to v4.41.0 (#760)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Brandon Treston <btreston@bitwarden.com>
2025-05-19 11:46:23 -04:00
renovate[bot]
2d80fceb8c [deps]: Update jest-preset-angular to v14.5.5 (#745)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Brandon Treston <btreston@bitwarden.com>
2025-05-19 11:02:43 -04:00
renovate[bot]
0489f0cbe9 [deps]: Update angular-cli monorepo to v17.3.17 (#742)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Brandon Treston <btreston@bitwarden.com>
2025-05-19 10:19:53 -04:00
Brandon Treston
c5d4cb9fb6 fix null error (#782) 2025-05-14 11:12:01 -04:00
Vincent Salucci
16d6647090 chore: bump version to v2025.5.0 (#778) 2025-05-06 11:52:17 -05:00
renovate[bot]
a08673917b [deps]: Update prettier to v3.5.3 (#758)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-05 10:52:46 +10:00
renovate[bot]
27e1ab9bcf [deps]: Update rxjs to v7.8.2 (#746)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-05 10:32:24 +10:00
Brandon Treston
3573e201a6 [PM-20134] Fix overwriteExisting and largeImport causing users to be deleted (#737)
* Fix mixed up bools, use whole object

* disallow overwriteExisting on large syncs

* remove unused file

* add test, always set overwriteExisting to false for batched requests

* add more tests

* wip

* Clean up

---------

Co-authored-by: Thomas Rittson <trittson@bitwarden.com>
2025-04-30 09:26:15 -04:00
Brandon Treston
23d285a9f6 change g suite propertey name in example to match the docs (#739) 2025-04-15 09:51:08 -04:00
renovate[bot]
527d2cb75d [deps]: Lock file maintenance (#738)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-15 08:43:39 -05:00
Andy Pixley
42efd689e3 [BRE-773] Fixing windows signing cert (#736) 2025-04-09 23:46:47 -04:00
Thomas Rittson
2fe980dea6 Bump version to 2024.4.0 (#735)
Note that this is effectively a re-release of 2025.1.0
in order to rollback from defective release 2025.3.0. 
rc will branch from 2025.1.0 and have this cherry-picked into it.
2025-04-10 12:53:21 +10:00
Matt Bishop
9446eedec7 Remove references to Codecov token (#734) 2025-03-27 15:51:22 -07:00
Vicki League
41ee0d82d5 [CL-570] Remove deprecated icons from use (#727) 2025-03-27 09:48:07 -04:00
renovate[bot]
40a85bb875 [deps]: Lock file maintenance (#731)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-24 17:00:16 +00:00
19 changed files with 1212 additions and 869 deletions

View File

@@ -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: |

View File

@@ -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 }}

View File

@@ -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 }}

View File

@@ -8,16 +8,12 @@ export class OrganizationImportRequest {
overwriteExisting = false;
largeImport = false;
constructor(
model:
| {
groups: Required<OrganizationImportGroupRequest>[];
users: Required<OrganizationImportMemberRequest>[];
overwriteExisting: boolean;
largeImport: boolean;
}
| ImportDirectoryRequest,
) {
constructor(model: {
groups: Required<OrganizationImportGroupRequest>[];
users: Required<OrganizationImportMemberRequest>[];
overwriteExisting: boolean;
largeImport: boolean;
}) {
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

File diff suppressed because it is too large Load Diff

View File

@@ -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"
},

View File

@@ -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[];
}

View File

@@ -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>

View File

@@ -614,7 +614,7 @@
{{ "ex" | i18n }} exclude:joe&#64;company.com | profile.firstName eq "John"
</div>
<div class="form-text" *ngIf="directory === directoryType.GSuite">
{{ "ex" | i18n }} exclude:joe&#64;company.com | orgName=Engineering
{{ "ex" | i18n }} exclude:joe&#64;company.com | orgUnitPath=/Engineering
</div>
</div>
<div class="mb-3" [hidden]="directory != directoryType.Ldap">

View File

@@ -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>

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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([]);
});

View 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);
});
});

View File

@@ -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,
}),
];

View 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;
});
});
});

View File

@@ -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

View File

@@ -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);
}
}

View 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;
}