Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PM-14360] Import Batching #703

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
19 changes: 19 additions & 0 deletions jslib/angular/src/services/jslib-services.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ import { StateService } from "@/jslib/common/src/services/state.service";
import { StateMigrationService } from "@/jslib/common/src/services/stateMigration.service";
import { TokenService } from "@/jslib/common/src/services/token.service";

import { DirectoryFactoryService } from "@/src/abstractions/directory-factory.service";
import { StateService as StateServiceExtended } from "@/src/abstractions/state.service";
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like this naming... open to suggestions. Alternatively we could rename the existing state service since we have 2 (one in jslib and one in src)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The src StateService extends the jslib StateService, so you can just use the extended class everywhere and it'll satisfy the type constraints.

However the better answer is that you don't need to add to jslib-services.module.ts. Like the rest of the jslib folder, it's essentially deprecated (but I acknowledge this isn't documented anywhere). Register your new dependencies in src/app/services/services.module.ts. That file only references the src StateService.

import { DefaultBatchRequestBuilder } from "@/src/services/default-batch-request-builder";
import { DefaultSingleRequestBuilder } from "@/src/services/default-single-request-builder";
import { DefaultDirectoryFactoryService } from "@/src/services/directory-factory.service";

import {
SafeInjectionToken,
SECURE_STORAGE,
Expand Down Expand Up @@ -138,6 +144,19 @@ import { ValidationService } from "./validation.service";
),
deps: [StorageServiceAbstraction, SECURE_STORAGE],
}),
safeProvider({
provide: DefaultBatchRequestBuilder,
deps: []
}),
safeProvider({
provide: DefaultSingleRequestBuilder,
deps: []
}),
safeProvider({
provide: DirectoryFactoryService,
useClass: DefaultDirectoryFactoryService,
deps: [LogService, I18nServiceAbstraction, StateServiceExtended],
}),
] satisfies SafeProvider[],
})
export class JslibServicesModule {}
6 changes: 3 additions & 3 deletions jslib/common/src/abstractions/api.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { ApiTokenRequest } from "../models/request/identityToken/apiTokenRequest
import { PasswordTokenRequest } from "../models/request/identityToken/passwordTokenRequest";
import { SsoTokenRequest } from "../models/request/identityToken/ssoTokenRequest";
import { OrganizationImportRequest } from "../models/request/organizationImportRequest";
import { IdentityCaptchaResponse } from '../models/response/identityCaptchaResponse';
import { IdentityTokenResponse } from '../models/response/identityTokenResponse';
import { IdentityTwoFactorResponse } from '../models/response/identityTwoFactorResponse';
import { IdentityCaptchaResponse } from "../models/response/identityCaptchaResponse";
import { IdentityTokenResponse } from "../models/response/identityTokenResponse";
import { IdentityTwoFactorResponse } from "../models/response/identityTwoFactorResponse";

export abstract class ApiService {
postIdentityToken: (
Expand Down
6 changes: 6 additions & 0 deletions src/abstractions/directory-factory.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { DirectoryType } from "@/src/enums/directoryType";
import { IDirectoryService } from "@/src/services/directory.service";

export abstract class DirectoryFactoryService {

Check warning on line 4 in src/abstractions/directory-factory.service.ts

View check run for this annotation

Codecov / codecov/patch

src/abstractions/directory-factory.service.ts#L4

Added line #L4 was not covered by tests
abstract createService(type: DirectoryType): IDirectoryService;
}
13 changes: 13 additions & 0 deletions src/abstractions/request-builder.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { OrganizationImportRequest } from "@/jslib/common/src/models/request/organizationImportRequest";

import { GroupEntry } from "@/src/models/groupEntry";
import { UserEntry } from "@/src/models/userEntry";

export abstract class RequestBuilderAbstratction {

Check warning on line 6 in src/abstractions/request-builder.service.ts

View check run for this annotation

Codecov / codecov/patch

src/abstractions/request-builder.service.ts#L6

Added line #L6 was not covered by tests
buildRequest: (
groups: GroupEntry[],
users: UserEntry[],
removeDisabled: boolean,
overwriteExisting: boolean,
) => OrganizationImportRequest[];
}
8 changes: 7 additions & 1 deletion src/app/services/services.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
import { NodeApiService } from "@/jslib/node/src/services/nodeApi.service";
import { NodeCryptoFunctionService } from "@/jslib/node/src/services/nodeCryptoFunction.service";

import { DirectoryFactoryService } from "@/src/abstractions/directory-factory.service";
import { DefaultBatchRequestBuilder } from "@/src/services/default-batch-request-builder";
import { DefaultSingleRequestBuilder } from "@/src/services/default-single-request-builder";

Check warning on line 30 in src/app/services/services.module.ts

View check run for this annotation

Codecov / codecov/patch

src/app/services/services.module.ts#L28-L30

Added lines #L28 - L30 were not covered by tests

import { AuthService as AuthServiceAbstraction } from "../../abstractions/auth.service";
import { StateService as StateServiceAbstraction } from "../../abstractions/state.service";
import { Account } from "../../models/account";
Expand Down Expand Up @@ -168,13 +172,15 @@
provide: SyncService,
useClass: SyncService,
deps: [
LogServiceAbstraction,
CryptoFunctionServiceAbstraction,
ApiServiceAbstraction,
MessagingServiceAbstraction,
I18nServiceAbstraction,
EnvironmentServiceAbstraction,
StateServiceAbstraction,
DefaultBatchRequestBuilder,
DefaultSingleRequestBuilder,
DirectoryFactoryService,
],
}),
safeProvider(AuthGuardService),
Expand Down
20 changes: 19 additions & 1 deletion src/bwdc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,13 @@
import { NodeApiService } from "@/jslib/node/src/services/nodeApi.service";
import { NodeCryptoFunctionService } from "@/jslib/node/src/services/nodeCryptoFunction.service";

import { DirectoryFactoryService } from "./abstractions/directory-factory.service";
import { Account } from "./models/account";
import { Program } from "./program";
import { AuthService } from "./services/auth.service";
import { DefaultBatchRequestBuilder } from "./services/default-batch-request-builder";
import { DefaultSingleRequestBuilder } from "./services/default-single-request-builder";
import { DefaultDirectoryFactoryService } from "./services/directory-factory.service";

Check warning on line 26 in src/bwdc.ts

View check run for this annotation

Codecov / codecov/patch

src/bwdc.ts#L24-L26

Added lines #L24 - L26 were not covered by tests
import { I18nService } from "./services/i18n.service";
import { KeytarSecureStorageService } from "./services/keytarSecureStorage.service";
import { LowdbStorageService } from "./services/lowdbStorage.service";
Expand Down Expand Up @@ -51,6 +55,9 @@
syncService: SyncService;
stateService: StateService;
stateMigrationService: StateMigrationService;
directoryFactoryService: DirectoryFactoryService;
batchRequestBuilder: DefaultBatchRequestBuilder;
singleRequestBuilder: DefaultSingleRequestBuilder;

constructor() {
const applicationName = "Bitwarden Directory Connector";
Expand Down Expand Up @@ -146,14 +153,25 @@
this.stateService,
);

this.syncService = new SyncService(
this.directoryFactoryService = new DefaultDirectoryFactoryService(

Check warning on line 156 in src/bwdc.ts

View check run for this annotation

Codecov / codecov/patch

src/bwdc.ts#L156

Added line #L156 was not covered by tests
this.logService,
this.i18nService,
this.stateService,
);

this.batchRequestBuilder = new DefaultBatchRequestBuilder();
this.singleRequestBuilder = new DefaultSingleRequestBuilder();

Check warning on line 163 in src/bwdc.ts

View check run for this annotation

Codecov / codecov/patch

src/bwdc.ts#L162-L163

Added lines #L162 - L163 were not covered by tests

this.syncService = new SyncService(

Check warning on line 165 in src/bwdc.ts

View check run for this annotation

Codecov / codecov/patch

src/bwdc.ts#L165

Added line #L165 was not covered by tests
this.cryptoFunctionService,
this.apiService,
this.messagingService,
this.i18nService,
this.environmentService,
this.stateService,
this.batchRequestBuilder,
this.singleRequestBuilder,
this.directoryFactoryService,
);

this.program = new Program(this);
Expand Down
70 changes: 70 additions & 0 deletions src/services/default-batch-request-builder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { OrganizationImportRequest } from "@/jslib/common/src/models/request/organizationImportRequest";

Check warning on line 1 in src/services/default-batch-request-builder.ts

View check run for this annotation

Codecov / codecov/patch

src/services/default-batch-request-builder.ts#L1

Added line #L1 was not covered by tests

import { GroupEntry } from "@/src/models/groupEntry";
import { UserEntry } from "@/src/models/userEntry";

import { RequestBuilderAbstratction } from "../abstractions/request-builder.service";

import { batchSize } from "./sync.service";

Check warning on line 8 in src/services/default-batch-request-builder.ts

View check run for this annotation

Codecov / codecov/patch

src/services/default-batch-request-builder.ts#L8

Added line #L8 was not covered by tests

/**
* This class is responsible for batching large sync requests (>2k users) into multiple smaller
* requests to the /import REST endpoint. This is done to ensure we are under the default
* maximum packet size for NGINX web servers to avoid the request potentially timing out
* */
export class DefaultBatchRequestBuilder implements RequestBuilderAbstratction {

Check warning on line 15 in src/services/default-batch-request-builder.ts

View check run for this annotation

Codecov / codecov/patch

src/services/default-batch-request-builder.ts#L15

Added line #L15 was not covered by tests
buildRequest(
groups: GroupEntry[],
users: UserEntry[],
removeDisabled: boolean,
overwriteExisting: boolean,
): OrganizationImportRequest[] {
const requests: OrganizationImportRequest[] = [];

Check warning on line 22 in src/services/default-batch-request-builder.ts

View check run for this annotation

Codecov / codecov/patch

src/services/default-batch-request-builder.ts#L22

Added line #L22 was not covered by tests

if (users.length > 0) {
const usersRequest = users.map((u) => {
return {

Check warning on line 26 in src/services/default-batch-request-builder.ts

View check run for this annotation

Codecov / codecov/patch

src/services/default-batch-request-builder.ts#L25-L26

Added lines #L25 - L26 were not covered by tests
email: u.email,
externalId: u.externalId,
deleted: u.deleted || (removeDisabled && u.disabled),
};
});

// Partition users
for (let i = 0; i < usersRequest.length; i += batchSize) {
const u = usersRequest.slice(i, i + batchSize);
const req = new OrganizationImportRequest({

Check warning on line 36 in src/services/default-batch-request-builder.ts

View check run for this annotation

Codecov / codecov/patch

src/services/default-batch-request-builder.ts#L34-L36

Added lines #L34 - L36 were not covered by tests
groups: [],
users: u,
largeImport: true,
overwriteExisting,
});
requests.push(req);

Check warning on line 42 in src/services/default-batch-request-builder.ts

View check run for this annotation

Codecov / codecov/patch

src/services/default-batch-request-builder.ts#L42

Added line #L42 was not covered by tests
}
}

if (groups.length > 0) {
const groupRequest = groups.map((g) => {
return {

Check warning on line 48 in src/services/default-batch-request-builder.ts

View check run for this annotation

Codecov / codecov/patch

src/services/default-batch-request-builder.ts#L47-L48

Added lines #L47 - L48 were not covered by tests
name: g.name,
externalId: g.externalId,
memberExternalIds: Array.from(g.userMemberExternalIds),
};
});

// Partition groups
for (let i = 0; i < groupRequest.length; i += batchSize) {
const g = groupRequest.slice(i, i + batchSize);
const req = new OrganizationImportRequest({

Check warning on line 58 in src/services/default-batch-request-builder.ts

View check run for this annotation

Codecov / codecov/patch

src/services/default-batch-request-builder.ts#L56-L58

Added lines #L56 - L58 were not covered by tests
groups: g,
users: [],
largeImport: true,
overwriteExisting,
});
requests.push(req);

Check warning on line 64 in src/services/default-batch-request-builder.ts

View check run for this annotation

Codecov / codecov/patch

src/services/default-batch-request-builder.ts#L64

Added line #L64 was not covered by tests
}
}

return requests;

Check warning on line 68 in src/services/default-batch-request-builder.ts

View check run for this annotation

Codecov / codecov/patch

src/services/default-batch-request-builder.ts#L68

Added line #L68 was not covered by tests
}
}
47 changes: 47 additions & 0 deletions src/services/default-batch-requests-builder.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { GroupEntry } from "@/src/models/groupEntry";
import { UserEntry } from "@/src/models/userEntry";

import { DefaultBatchRequestBuilder } from "./default-batch-request-builder";
import { DefaultSingleRequestBuilder } from "./default-single-request-builder";

describe("BatchingService", () => {
Copy link
Member

@eliykat eliykat Jan 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

describe block is still using the old name

let batchRequestBuilder: DefaultBatchRequestBuilder;
let singleRequestBuilder: DefaultSingleRequestBuilder;

function userSimulator(userCount: number) {
return Array(userCount).fill(new UserEntry());
}

function groupSimulator(groupCount: number) {
return Array(groupCount).fill(new GroupEntry());
}

beforeEach(async () => {
batchRequestBuilder = new DefaultBatchRequestBuilder();
singleRequestBuilder = new DefaultSingleRequestBuilder();
});

it("BatchRequestBuilder batches requests for > 2000 users", () => {
const mockGroups = groupSimulator(11000);
const mockUsers = userSimulator(11000);

const requests = batchRequestBuilder.buildRequest(mockGroups, mockUsers, true, true);

expect(requests.length).toEqual(12);
});

it("SingleRequestBuilder returns single request for 200 users", () => {
const mockGroups = groupSimulator(200);
const mockUsers = userSimulator(200);

const requests = singleRequestBuilder.buildRequest(mockGroups, mockUsers, true, true);

expect(requests.length).toEqual(1);
});

it("BatchRequestBuilder retuns an empty array when there are no users or groups", () => {
const requests = batchRequestBuilder.buildRequest([], [], true, true);

expect(requests).toEqual([]);
});
});
42 changes: 42 additions & 0 deletions src/services/default-single-request-builder.ts
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not a big deal, but I could've been more explicit re: naming.

If you had only 1 request builder class, you might do something like:

DefaultRequestBuilder extends RequestBuilder

This is a most classes where you only have a single implementation.

However, here we do have multiple implementations and we already have a way to distinguish them:

SingleRequestBuilder extends RequestBuilder
BatchRequestBuilder extends RequestBuilder
// there is no "default" implementation, you have to choose one or the other

Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { OrganizationImportRequest } from "@/jslib/common/src/models/request/organizationImportRequest";

Check warning on line 1 in src/services/default-single-request-builder.ts

View check run for this annotation

Codecov / codecov/patch

src/services/default-single-request-builder.ts#L1

Added line #L1 was not covered by tests

import { GroupEntry } from "@/src/models/groupEntry";
import { UserEntry } from "@/src/models/userEntry";

import { RequestBuilderAbstratction } from "../abstractions/request-builder.service";

/**
* This class is responsible for building small (<2k users) syncs as a single
* request to the /import endpoint. This is done to be backwards compatible with
* existing functionality for sync requests that are sufficiently small enough to not
* exceed default maximum packet size limits on NGINX web servers.
* */
export class DefaultSingleRequestBuilder implements RequestBuilderAbstratction {

Check warning on line 14 in src/services/default-single-request-builder.ts

View check run for this annotation

Codecov / codecov/patch

src/services/default-single-request-builder.ts#L14

Added line #L14 was not covered by tests
buildRequest(
groups: GroupEntry[],
users: UserEntry[],
removeDisabled: boolean,
overwriteExisting: boolean,
): OrganizationImportRequest[] {
return [

Check warning on line 21 in src/services/default-single-request-builder.ts

View check run for this annotation

Codecov / codecov/patch

src/services/default-single-request-builder.ts#L21

Added line #L21 was not covered by tests
new OrganizationImportRequest({
groups: (groups ?? []).map((g) => {
return {

Check warning on line 24 in src/services/default-single-request-builder.ts

View check run for this annotation

Codecov / codecov/patch

src/services/default-single-request-builder.ts#L24

Added line #L24 was not covered by tests
name: g.name,
externalId: g.externalId,
memberExternalIds: Array.from(g.userMemberExternalIds),
};
}),
users: (users ?? []).map((u) => {
return {

Check warning on line 31 in src/services/default-single-request-builder.ts

View check run for this annotation

Codecov / codecov/patch

src/services/default-single-request-builder.ts#L31

Added line #L31 was not covered by tests
email: u.email,
externalId: u.externalId,
deleted: u.deleted || (removeDisabled && u.disabled),
};
}),
overwriteExisting: overwriteExisting,
largeImport: false,
}),
];
}
}
37 changes: 37 additions & 0 deletions src/services/directory-factory.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
import { LogService } from "@/jslib/common/src/abstractions/log.service";

import { DirectoryFactoryService } from "../abstractions/directory-factory.service";
import { StateService } from "../abstractions/state.service";
import { DirectoryType } from "../enums/directoryType";

Check warning on line 6 in src/services/directory-factory.service.ts

View check run for this annotation

Codecov / codecov/patch

src/services/directory-factory.service.ts#L6

Added line #L6 was not covered by tests

import { AzureDirectoryService } from "./azure-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";

Check warning on line 12 in src/services/directory-factory.service.ts

View check run for this annotation

Codecov / codecov/patch

src/services/directory-factory.service.ts#L8-L12

Added lines #L8 - L12 were not covered by tests

export class DefaultDirectoryFactoryService implements DirectoryFactoryService {

Check warning on line 14 in src/services/directory-factory.service.ts

View check run for this annotation

Codecov / codecov/patch

src/services/directory-factory.service.ts#L14

Added line #L14 was not covered by tests
constructor(
private logService: LogService,
private i18nService: I18nService,
private stateService: StateService,

Check warning on line 18 in src/services/directory-factory.service.ts

View check run for this annotation

Codecov / codecov/patch

src/services/directory-factory.service.ts#L16-L18

Added lines #L16 - L18 were not covered by tests
) {}

createService(directoryType: DirectoryType) {
switch (directoryType) {
case DirectoryType.GSuite:
return new GSuiteDirectoryService(this.logService, this.i18nService, this.stateService);

Check warning on line 24 in src/services/directory-factory.service.ts

View check run for this annotation

Codecov / codecov/patch

src/services/directory-factory.service.ts#L24

Added line #L24 was not covered by tests
case DirectoryType.AzureActiveDirectory:
return new AzureDirectoryService(this.logService, this.i18nService, this.stateService);

Check warning on line 26 in src/services/directory-factory.service.ts

View check run for this annotation

Codecov / codecov/patch

src/services/directory-factory.service.ts#L26

Added line #L26 was not covered by tests
case DirectoryType.Ldap:
return new LdapDirectoryService(this.logService, this.i18nService, this.stateService);

Check warning on line 28 in src/services/directory-factory.service.ts

View check run for this annotation

Codecov / codecov/patch

src/services/directory-factory.service.ts#L28

Added line #L28 was not covered by tests
case DirectoryType.Okta:
return new OktaDirectoryService(this.logService, this.i18nService, this.stateService);

Check warning on line 30 in src/services/directory-factory.service.ts

View check run for this annotation

Codecov / codecov/patch

src/services/directory-factory.service.ts#L30

Added line #L30 was not covered by tests
case DirectoryType.OneLogin:
return new OneLoginDirectoryService(this.logService, this.i18nService, this.stateService);

Check warning on line 32 in src/services/directory-factory.service.ts

View check run for this annotation

Codecov / codecov/patch

src/services/directory-factory.service.ts#L32

Added line #L32 was not covered by tests
default:
throw new Error("Invalid Directory Type");

Check warning on line 34 in src/services/directory-factory.service.ts

View check run for this annotation

Codecov / codecov/patch

src/services/directory-factory.service.ts#L34

Added line #L34 was not covered by tests
}
}
}
Loading
Loading