mirror of
https://github.com/bitwarden/directory-connector
synced 2025-12-05 23:53:21 +00:00
[PM-13008] Add ldap integration tests (#637)
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { Entry } from "./entry";
|
||||
import { UserEntry } from "./userEntry";
|
||||
|
||||
@@ -14,4 +16,38 @@ export class GroupEntry extends Entry {
|
||||
|
||||
return this.name;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
name: this.name,
|
||||
referenceId: this.referenceId,
|
||||
externalId: this.externalId,
|
||||
userMemberExternalIds:
|
||||
this.userMemberExternalIds == null ? null : [...this.userMemberExternalIds],
|
||||
groupMemberReferenceIds:
|
||||
this.groupMemberReferenceIds == null ? null : [...this.groupMemberReferenceIds],
|
||||
users: this.users?.map((u) => u.toJSON()),
|
||||
};
|
||||
}
|
||||
|
||||
static fromJSON(data: Jsonify<GroupEntry>) {
|
||||
const result = new GroupEntry();
|
||||
result.referenceId = data.referenceId;
|
||||
result.externalId = data.externalId;
|
||||
result.name = data.name;
|
||||
|
||||
if (data.userMemberExternalIds != null) {
|
||||
result.userMemberExternalIds = new Set(data.userMemberExternalIds);
|
||||
}
|
||||
|
||||
if (data.groupMemberReferenceIds != null) {
|
||||
result.groupMemberReferenceIds = new Set(data.groupMemberReferenceIds);
|
||||
}
|
||||
|
||||
if (data.users != null) {
|
||||
result.users = data.users.map((u) => UserEntry.fromJSON(u));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { Entry } from "./entry";
|
||||
|
||||
export class UserEntry extends Entry {
|
||||
@@ -12,4 +14,26 @@ export class UserEntry extends Entry {
|
||||
|
||||
return this.email;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
referenceId: this.referenceId,
|
||||
externalId: this.externalId,
|
||||
email: this.email,
|
||||
disabled: this.disabled,
|
||||
deleted: this.deleted,
|
||||
};
|
||||
}
|
||||
|
||||
static fromJSON(data: Jsonify<UserEntry>) {
|
||||
const result = new UserEntry();
|
||||
result.referenceId = data.referenceId;
|
||||
result.externalId = data.externalId;
|
||||
|
||||
result.email = data.email;
|
||||
result.disabled = data.disabled;
|
||||
result.deleted = data.deleted;
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
207
src/services/ldap-directory.service.integration.spec.ts
Normal file
207
src/services/ldap-directory.service.integration.spec.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
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 { DirectoryType } from "../enums/directoryType";
|
||||
import { LdapConfiguration } from "../models/ldapConfiguration";
|
||||
import { SyncConfiguration } from "../models/syncConfiguration";
|
||||
|
||||
import { LdapDirectoryService } from "./ldap-directory.service";
|
||||
import { StateService } from "./state.service";
|
||||
|
||||
// These tests integrate with the OpenLDAP docker image and seed data located in the openldap folder.
|
||||
// To run theses tests:
|
||||
// Install mkcert, e.g.: brew install mkcert
|
||||
// Configure the environment: npm run test:integration:setup
|
||||
// Run tests: npm run test:integration:watch
|
||||
|
||||
describe("ldapDirectoryService", () => {
|
||||
let logService: MockProxy<LogService>;
|
||||
let i18nService: MockProxy<I18nService>;
|
||||
let stateService: MockProxy<StateService>;
|
||||
|
||||
let directoryService: LdapDirectoryService;
|
||||
|
||||
beforeEach(() => {
|
||||
logService = mock();
|
||||
i18nService = mock();
|
||||
stateService = mock();
|
||||
|
||||
stateService.getDirectoryType.mockResolvedValue(DirectoryType.Ldap);
|
||||
stateService.getLastUserSync.mockResolvedValue(null); // do not filter results by last modified date
|
||||
i18nService.t.mockImplementation((id) => id); // passthrough implementation for any error messages
|
||||
|
||||
directoryService = new LdapDirectoryService(logService, i18nService, stateService);
|
||||
});
|
||||
|
||||
describe("basic sync fetching users and groups", () => {
|
||||
it("with an unencrypted connection", async () => {
|
||||
stateService.getDirectory
|
||||
.calledWith(DirectoryType.Ldap)
|
||||
.mockResolvedValue(getLdapConfiguration());
|
||||
stateService.getSync.mockResolvedValue(getSyncConfiguration({ groups: true, users: true }));
|
||||
|
||||
const result = await directoryService.getEntries(true, true);
|
||||
expect(result).toEqual([groupFixtures, userFixtures]);
|
||||
});
|
||||
|
||||
// StartTLS opportunistically encrypts an otherwise unencrypted connection and therefore uses the same port
|
||||
it("with StartTLS + SSL", async () => {
|
||||
stateService.getDirectory.calledWith(DirectoryType.Ldap).mockResolvedValue(
|
||||
getLdapConfiguration({
|
||||
ssl: true,
|
||||
startTls: true,
|
||||
tlsCaPath: "./openldap/certs/rootCA.pem",
|
||||
}),
|
||||
);
|
||||
stateService.getSync.mockResolvedValue(getSyncConfiguration({ groups: true, users: true }));
|
||||
|
||||
const result = await directoryService.getEntries(true, true);
|
||||
expect(result).toEqual([groupFixtures, userFixtures]);
|
||||
});
|
||||
|
||||
// The ldaps protocol requires use of SSL and uses the secure port
|
||||
it("with SSL using the ldaps protocol", async () => {
|
||||
stateService.getDirectory.calledWith(DirectoryType.Ldap).mockResolvedValue(
|
||||
getLdapConfiguration({
|
||||
port: 1636,
|
||||
ssl: true,
|
||||
sslCaPath: "./openldap/certs/rootCA.pem",
|
||||
}),
|
||||
);
|
||||
stateService.getSync.mockResolvedValue(getSyncConfiguration({ groups: true, users: true }));
|
||||
|
||||
const result = await directoryService.getEntries(true, true);
|
||||
expect(result).toEqual([groupFixtures, userFixtures]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("users", () => {
|
||||
it("respects the users path", async () => {
|
||||
stateService.getDirectory
|
||||
.calledWith(DirectoryType.Ldap)
|
||||
.mockResolvedValue(getLdapConfiguration());
|
||||
stateService.getSync.mockResolvedValue(
|
||||
getSyncConfiguration({
|
||||
users: true,
|
||||
userPath: "ou=Human Resources",
|
||||
}),
|
||||
);
|
||||
|
||||
// These users are in the Human Resources ou
|
||||
const hrUsers = userFixtures.filter(
|
||||
(u) =>
|
||||
u.referenceId === "cn=Roland Dyke,ou=Human Resources,dc=bitwarden,dc=com" ||
|
||||
u.referenceId === "cn=Charin Goulfine,ou=Human Resources,dc=bitwarden,dc=com" ||
|
||||
u.referenceId === "cn=Angelle Guarino,ou=Human Resources,dc=bitwarden,dc=com",
|
||||
);
|
||||
|
||||
const result = await directoryService.getEntries(true, true);
|
||||
expect(result[1]).toEqual(expect.arrayContaining(hrUsers));
|
||||
expect(result[1].length).toEqual(hrUsers.length);
|
||||
});
|
||||
|
||||
it("filters users", async () => {
|
||||
stateService.getDirectory
|
||||
.calledWith(DirectoryType.Ldap)
|
||||
.mockResolvedValue(getLdapConfiguration());
|
||||
stateService.getSync.mockResolvedValue(
|
||||
getSyncConfiguration({ users: true, userFilter: "(cn=Roland Dyke)" }),
|
||||
);
|
||||
|
||||
const roland = userFixtures.find(
|
||||
(u) => u.referenceId === "cn=Roland Dyke,ou=Human Resources,dc=bitwarden,dc=com",
|
||||
);
|
||||
const result = await directoryService.getEntries(true, true);
|
||||
expect(result).toEqual([undefined, [roland]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("groups", () => {
|
||||
it("respects the groups path", async () => {
|
||||
stateService.getDirectory
|
||||
.calledWith(DirectoryType.Ldap)
|
||||
.mockResolvedValue(getLdapConfiguration());
|
||||
stateService.getSync.mockResolvedValue(
|
||||
getSyncConfiguration({
|
||||
groups: true,
|
||||
groupPath: "ou=Janitorial",
|
||||
}),
|
||||
);
|
||||
|
||||
// These groups are in the Janitorial ou
|
||||
const janitorialGroups = groupFixtures.filter((g) => g.name === "Cleaners");
|
||||
|
||||
const result = await directoryService.getEntries(true, true);
|
||||
expect(result).toEqual([janitorialGroups, undefined]);
|
||||
});
|
||||
|
||||
it("filters groups", async () => {
|
||||
stateService.getDirectory
|
||||
.calledWith(DirectoryType.Ldap)
|
||||
.mockResolvedValue(getLdapConfiguration());
|
||||
stateService.getSync.mockResolvedValue(
|
||||
getSyncConfiguration({ groups: true, groupFilter: "(cn=Red Team)" }),
|
||||
);
|
||||
|
||||
const redTeam = groupFixtures.find(
|
||||
(u) => u.referenceId === "cn=Red Team,dc=bitwarden,dc=com",
|
||||
);
|
||||
const result = await directoryService.getEntries(true, true);
|
||||
expect(result).toEqual([[redTeam], undefined]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @returns a basic ldap configuration without TLS/SSL enabled. Can be overridden by passing in a partial configuration.
|
||||
*/
|
||||
const getLdapConfiguration = (config?: Partial<LdapConfiguration>): LdapConfiguration => ({
|
||||
ssl: false,
|
||||
startTls: false,
|
||||
tlsCaPath: null,
|
||||
sslAllowUnauthorized: false,
|
||||
sslCertPath: null,
|
||||
sslKeyPath: null,
|
||||
sslCaPath: null,
|
||||
hostname: "localhost",
|
||||
port: 1389,
|
||||
domain: null,
|
||||
rootPath: "dc=bitwarden,dc=com",
|
||||
currentUser: false,
|
||||
username: "cn=admin,dc=bitwarden,dc=com",
|
||||
password: "admin",
|
||||
ad: false,
|
||||
pagedSearch: false,
|
||||
...(config ?? {}),
|
||||
});
|
||||
|
||||
/**
|
||||
* @returns a basic sync configuration. Can be overridden by passing in a partial configuration.
|
||||
*/
|
||||
const getSyncConfiguration = (config?: Partial<SyncConfiguration>): SyncConfiguration => ({
|
||||
users: false,
|
||||
groups: false,
|
||||
interval: 5,
|
||||
userFilter: null,
|
||||
groupFilter: null,
|
||||
removeDisabled: false,
|
||||
overwriteExisting: false,
|
||||
largeImport: false,
|
||||
// Ldap properties
|
||||
groupObjectClass: "posixGroup",
|
||||
userObjectClass: "person",
|
||||
groupPath: null,
|
||||
userPath: null,
|
||||
groupNameAttribute: "cn",
|
||||
userEmailAttribute: "mail",
|
||||
memberAttribute: "memberUid",
|
||||
useEmailPrefixSuffix: false,
|
||||
emailPrefixAttribute: "sAMAccountName",
|
||||
emailSuffix: null,
|
||||
creationDateAttribute: "whenCreated",
|
||||
revisionDateAttribute: "whenChanged",
|
||||
...(config ?? {}),
|
||||
});
|
||||
Reference in New Issue
Block a user