Merge pull request #30594 from ganievs/feature/move-to-ldapts

Feature/move to ldapts
This commit is contained in:
Ben Lambert
2025-11-04 09:51:09 +01:00
committed by GitHub
14 changed files with 656 additions and 650 deletions
+70
View File
@@ -0,0 +1,70 @@
---
'@backstage/plugin-catalog-backend-module-ldap': minor
---
Moved from `ldapjs` dependency to `ldapts`
### Breaking Changes
**Type Migration**
Custom transformers must now accept `Entry` from ldapts instead of `SearchEntry`
from ldapjs The Entry type provides direct property access without need for
`.object()` or `.raw()` methods.
If you have custom user or group transformers, update the signature from:
```typescript
(vendor: LdapVendor, config: UserConfig, entry: SearchEntry) =>
Promise<UserEntity | undefined>;
```
to
```typescript
(vendor: LdapVendor, config: UserConfig, entry: Entry) =>
Promise<UserEntity | undefined>;
```
**Search Options**
Updated LDAP search configuration `typesOnly: false``attributeValues: true`
This inverts the boolean logic: ldapjs used negative form while ldapts uses
positive form. Both achieve the same result: retrieving attribute values rather
than just attribute names.
Update LDAP search options in configuration from
```yaml
options:
typesOnly: false
```
to
```yaml
options:
attributeValues: true
```
**API Changes** Removed `LdapClient.searchStreaming()` method. Users should
migrate to `LdapClient.search()` instead
If you're using `searchStreaming` directly:
```typescript
// Before
await client.searchStreaming(dn, options, async entry => {
// process each entry
});
// After
const entries = await client.search(dn, options);
for (const entry of entries) {
// process each entry
}
```
> **_NOTE:_**: Both methods have always loaded all entries into memory. The
> searchStreaming method was only needed internally to handle ldapjs's
> event-based API.
+10 -10
View File
@@ -85,7 +85,7 @@ export interface Config {
sizeLimit?: number;
timeLimit?: number;
derefAliases?: number;
typesOnly?: boolean;
returnAttributeValues?: boolean;
paged?:
| boolean
| {
@@ -165,7 +165,7 @@ export interface Config {
sizeLimit?: number;
timeLimit?: number;
derefAliases?: number;
typesOnly?: boolean;
returnAttributeValues?: boolean;
paged?:
| boolean
| {
@@ -250,7 +250,7 @@ export interface Config {
sizeLimit?: number;
timeLimit?: number;
derefAliases?: number;
typesOnly?: boolean;
returnAttributeValues?: boolean;
paged?:
| boolean
| {
@@ -340,7 +340,7 @@ export interface Config {
sizeLimit?: number;
timeLimit?: number;
derefAliases?: number;
typesOnly?: boolean;
returnAttributeValues?: boolean;
paged?:
| boolean
| {
@@ -500,7 +500,7 @@ export interface Config {
sizeLimit?: number;
timeLimit?: number;
derefAliases?: number;
typesOnly?: boolean;
returnAttributeValues?: boolean;
paged?:
| boolean
| {
@@ -580,7 +580,7 @@ export interface Config {
sizeLimit?: number;
timeLimit?: number;
derefAliases?: number;
typesOnly?: boolean;
returnAttributeValues?: boolean;
paged?:
| boolean
| {
@@ -666,7 +666,7 @@ export interface Config {
sizeLimit?: number;
timeLimit?: number;
derefAliases?: number;
typesOnly?: boolean;
returnAttributeValues?: boolean;
paged?:
| boolean
| {
@@ -756,7 +756,7 @@ export interface Config {
sizeLimit?: number;
timeLimit?: number;
derefAliases?: number;
typesOnly?: boolean;
returnAttributeValues?: boolean;
paged?:
| boolean
| {
@@ -914,7 +914,7 @@ export interface Config {
sizeLimit?: number;
timeLimit?: number;
derefAliases?: number;
typesOnly?: boolean;
returnAttributeValues?: boolean;
paged?:
| boolean
| {
@@ -997,7 +997,7 @@ export interface Config {
sizeLimit?: number;
timeLimit?: number;
derefAliases?: number;
typesOnly?: boolean;
returnAttributeValues?: boolean;
paged?:
| boolean
| {
@@ -45,8 +45,7 @@
"@backstage/plugin-catalog-common": "workspace:^",
"@backstage/plugin-catalog-node": "workspace:^",
"@backstage/types": "workspace:^",
"@types/ldapjs": "^2.2.5",
"ldapjs": "^2.3.3",
"ldapts": "^8.0.6",
"lodash": "^4.17.21",
"uuid": "^11.0.0"
},
@@ -6,10 +6,11 @@
import { BackendFeature } from '@backstage/backend-plugin-api';
import { CatalogProcessor } from '@backstage/plugin-catalog-node';
import { CatalogProcessorEmit } from '@backstage/plugin-catalog-node';
import { Client } from 'ldapjs';
import { Client } from 'ldapts';
import { Config } from '@backstage/config';
import { EntityProvider } from '@backstage/plugin-catalog-node';
import { EntityProviderConnection } from '@backstage/plugin-catalog-node';
import { Entry } from 'ldapts';
import { ExtensionPoint } from '@backstage/backend-plugin-api';
import { GroupEntity } from '@backstage/catalog-model';
import { JsonValue } from '@backstage/types';
@@ -18,8 +19,8 @@ import { LoggerService } from '@backstage/backend-plugin-api';
import { SchedulerService } from '@backstage/backend-plugin-api';
import { SchedulerServiceTaskRunner } from '@backstage/backend-plugin-api';
import { SchedulerServiceTaskScheduleDefinition } from '@backstage/backend-plugin-api';
import { SearchEntry } from 'ldapjs';
import { SearchOptions } from 'ldapjs';
import { SearchOptions } from 'ldapts';
import { SearchResult } from 'ldapts';
import { UserEntity } from '@backstage/catalog-model';
// @public
@@ -36,14 +37,14 @@ export default catalogModuleLdapOrgEntityProvider;
export function defaultGroupTransformer(
vendor: LdapVendor,
config: GroupConfig,
entry: SearchEntry,
entry: Entry,
): Promise<GroupEntity | undefined>;
// @public
export function defaultUserTransformer(
vendor: LdapVendor,
config: UserConfig,
entry: SearchEntry,
entry: Entry,
): Promise<UserEntity | undefined>;
// @public
@@ -70,7 +71,7 @@ export type GroupConfig = {
export type GroupTransformer = (
vendor: LdapVendor,
config: GroupConfig,
group: SearchEntry,
group: Entry,
) => Promise<GroupEntity | undefined>;
// @public
@@ -92,14 +93,9 @@ export class LdapClient {
bind?: BindConfig,
tls?: TLSConfig,
): Promise<LdapClient>;
getRootDSE(): Promise<SearchEntry | undefined>;
getRootDSE(): Promise<Entry | undefined>;
getVendor(): Promise<LdapVendor>;
search(dn: string, options: SearchOptions): Promise<SearchEntry[]>;
searchStreaming(
dn: string,
options: SearchOptions,
f: (entry: SearchEntry) => Promise<void> | void,
): Promise<void>;
search(dn: string, options: SearchOptions): Promise<SearchResult>;
}
// @public
@@ -203,12 +199,12 @@ export type LdapProviderConfig = {
export type LdapVendor = {
dnAttributeName: string;
uuidAttributeName: string;
decodeStringAttribute: (entry: SearchEntry, name: string) => string[];
decodeStringAttribute: (entry: Entry, name: string) => string[];
};
// @public
export function mapStringAttr(
entry: SearchEntry,
entry: Entry,
vendor: LdapVendor,
attributeName: string | undefined,
setter: (value: string) => void,
@@ -265,7 +261,7 @@ export type UserConfig = {
export type UserTransformer = (
vendor: LdapVendor,
config: UserConfig,
user: SearchEntry,
user: Entry,
) => Promise<UserEntity | undefined>;
// @public
@@ -14,21 +14,19 @@
* limitations under the License.
*/
import { ForwardedError, stringifyError } from '@backstage/errors';
import { ForwardedError } from '@backstage/errors';
import { readFile } from 'fs/promises';
import ldap, { Client, SearchEntry, SearchOptions } from 'ldapjs';
import { cloneDeep } from 'lodash';
import { Client, Entry, SearchOptions, SearchResult } from 'ldapts';
import tlsLib from 'tls';
import { BindConfig, TLSConfig } from './config';
import { createOptions, errorString } from './util';
import {
AEDirVendor,
ActiveDirectoryVendor,
DefaultLdapVendor,
GoogleLdapVendor,
LLDAPVendor,
FreeIpaVendor,
LdapVendor,
GoogleLdapVendor,
} from './vendors';
import { LoggerService } from '@backstage/backend-plugin-api';
@@ -39,6 +37,7 @@ import { LoggerService } from '@backstage/backend-plugin-api';
*
* @public
*/
export class LdapClient {
private vendor: Promise<LdapVendor> | undefined;
@@ -57,36 +56,31 @@ export class LdapClient {
key: key,
});
}
const client = ldap.createClient({
const tlsOptions: tlsLib.ConnectionOptions = {
secureContext,
rejectUnauthorized: tls?.rejectUnauthorized,
};
const client = new Client({
url: target,
tlsOptions: {
secureContext,
rejectUnauthorized: tls?.rejectUnauthorized,
},
...(Object.values(tlsOptions).some(v => v !== undefined)
? { tlsOptions }
: undefined),
});
// We want to have a catch-all error handler at the top, since the default
// behavior of the client is to blow up the entire process when it fails,
// unless an error handler is set.
client.on('error', (err: ldap.Error) => {
logger.warn(`LDAP client threw an error, ${errorString(err)}`);
});
const ldapClient = new LdapClient(client, logger);
if (!bind) {
return new LdapClient(client, logger);
if (bind) {
try {
await client.bind(bind.dn, bind.secret);
} catch (error) {
await client.unbind();
throw new ForwardedError(
`LDAP bind failed for ${bind.dn}, ${error}`,
error,
);
}
}
return new Promise<LdapClient>((resolve, reject) => {
const { dn, secret } = bind;
client.bind(dn, secret, err => {
if (err) {
reject(`LDAP bind failed for ${dn}, ${errorString(err)}`);
} else {
resolve(new LdapClient(client, logger));
}
});
});
return ldapClient;
}
private readonly client: Client;
@@ -103,129 +97,22 @@ export class LdapClient {
* @param dn - The fully qualified base DN to search within
* @param options - The search options
*/
async search(dn: string, options: SearchOptions): Promise<SearchEntry[]> {
async search(dn: string, options: SearchOptions): Promise<SearchResult> {
this.logger.debug(`Reading LDAP entries so far`);
try {
const output: SearchEntry[] = [];
const ldaptsOptions: SearchOptions = {
scope: options.scope,
filter: options.filter,
attributes: options.attributes,
sizeLimit: options.sizeLimit,
timeLimit: options.timeLimit,
derefAliases: options.derefAliases,
paged: options.paged,
};
const logInterval = setInterval(() => {
this.logger.debug(`Read ${output.length} LDAP entries so far...`);
}, 5000);
const result = await this.client.search(dn, ldaptsOptions);
const search = new Promise<SearchEntry[]>((resolve, reject) => {
// Note that we clone the (frozen) options, since ldapjs rudely tries to
// overwrite parts of them
this.client.search(dn, cloneDeep(options), (err, res) => {
if (err) {
reject(new Error(errorString(err)));
return;
}
res.on('searchReference', () => {
this.logger.warn('Received unsupported search referral');
});
res.on('searchEntry', entry => {
output.push(entry);
});
res.on('error', e => {
reject(new Error(errorString(e)));
});
res.on('page', (_result, cb) => {
if (cb) {
cb();
}
});
res.on('end', r => {
if (!r) {
reject(new Error('Null response'));
} else if (r.status !== 0) {
reject(new Error(`Got status ${r.status}: ${r.errorMessage}`));
} else {
resolve(output);
}
});
});
});
return await search.finally(() => {
clearInterval(logInterval);
});
} catch (e) {
throw new ForwardedError(`LDAP search at DN "${dn}" failed`, e);
}
}
/**
* Performs an LDAP search operation, calls a function on each entry to limit memory usage
*
* @param dn - The fully qualified base DN to search within
* @param options - The search options
* @param f - The callback to call on each search entry
*/
async searchStreaming(
dn: string,
options: SearchOptions,
f: (entry: SearchEntry) => Promise<void> | void,
): Promise<void> {
try {
return await new Promise<void>((resolve, reject) => {
// Note that we clone the (frozen) options, since ldapjs rudely tries to
// overwrite parts of them
this.client.search(dn, createOptions(options), (err, res) => {
if (err) {
reject(new Error(errorString(err)));
}
let awaitList: Array<Promise<void> | void> = [];
let transformError = false;
const transformReject = (e: Error) => {
transformError = true;
reject(
new Error(
`Transform function threw an exception, ${stringifyError(e)}`,
),
);
};
res.on('searchReference', () => {
this.logger.warn('Received unsupported search referral');
});
res.on('searchEntry', entry => {
if (!transformError) awaitList.push(f(entry));
});
res.on('page', (_, cb) => {
// awaits completion before fetching next page
Promise.all(awaitList)
.then(() => {
// flush list
awaitList = [];
if (cb) cb();
})
.catch(transformReject);
});
res.on('error', e => {
reject(new Error(errorString(e)));
});
res.on('end', r => {
if (!r) {
throw new Error('Null response');
} else if (r.status !== 0) {
throw new Error(`Got status ${r.status}: ${r.errorMessage}`);
} else {
Promise.all(awaitList)
.then(() => resolve())
.catch(transformReject);
}
});
});
});
return result;
} catch (e) {
throw new ForwardedError(`LDAP search at DN "${dn}" failed`, e);
}
@@ -241,18 +128,18 @@ export class LdapClient {
if (this.vendor) {
return this.vendor;
}
const clientHost = this.client?.host || '';
// const clientHost = this.client?.host || '';
this.vendor = this.getRootDSE()
.then(root => {
if (root && root.raw?.forestFunctionality) {
if (root && root.forestFunctionality) {
return ActiveDirectoryVendor;
} else if (root && root.raw?.ipaDomainLevel) {
} else if (root && root.ipaDomainLevel) {
return FreeIpaVendor;
} else if (root && 'aeRoot' in root.raw) {
} else if (root && 'aeRoot' in root) {
return AEDirVendor;
} else if (clientHost === 'ldap.google.com') {
} else if (this.isGoogleLDAP(root)) {
return GoogleLdapVendor;
} else if (root && root.raw?.vendorName?.toString() === 'LLDAP') {
} else if (root && root.vendorName?.toString() === 'LLDAP') {
return LLDAPVendor;
}
return DefaultLdapVendor;
@@ -264,18 +151,73 @@ export class LdapClient {
return this.vendor;
}
/**
* Check if the LDAP server is Google LDAP by examining RootDSE and schema
*/
private isGoogleLDAP(rootDSE: Entry | undefined): boolean {
if (!rootDSE) {
return false;
}
// RootDSE characteristics
const hasGoogleRootDSEPattern =
!rootDSE.namingContexts && // No namingContexts
!rootDSE.supportedControl && // No supportedControl
!rootDSE.vendorName && // No vendor info
!rootDSE.vendorVersion &&
rootDSE.subschemaSubentry === 'cn=subschema';
if (!hasGoogleRootDSEPattern) {
return false;
}
try {
const schemaHasGoogleAttributes = this.checkGoogleSchema(rootDSE);
return schemaHasGoogleAttributes;
} catch (error) {
throw new ForwardedError('Schema check failed', error);
}
}
// Check a shema for Google-specific patterns
private checkGoogleSchema(rootDSE: Entry): boolean {
try {
const objectClasses = this.parseSchemaValues(rootDSE.objectClasses);
const attributeTypes = this.parseSchemaValues(rootDSE.attributeTypes);
// Check if any Google-specific attributes are present
const hasGoogleAttributes =
objectClasses.some(oc => oc.includes('googleUid')) ||
attributeTypes.some(at => at.includes('googleAdminCreated'));
return hasGoogleAttributes;
} catch (error) {
this.logger.warn('Error checking schema:', error);
return false;
}
}
private parseSchemaValues(
schemaValue: Buffer | Buffer[] | string[] | string,
): string[] {
if (!schemaValue) return [];
const values = Array.isArray(schemaValue) ? schemaValue : [schemaValue];
return values.map(v => v.toString());
}
/**
* Get the Root DSE.
*
* @see https://ldapwiki.com/wiki/RootDSE
*/
async getRootDSE(): Promise<SearchEntry | undefined> {
const result = await this.search('', {
async getRootDSE(): Promise<Entry | undefined> {
const result = await this.client.search('', {
scope: 'base',
filter: '(objectclass=*)',
} as SearchOptions);
if (result && result.length === 1) {
return result[0];
if (result && result.searchEntries.length === 1) {
return result.searchEntries[0];
}
return undefined;
}
@@ -179,7 +179,7 @@ describe('readLdapConfig', () => {
timeLimit: 42,
sizeLimit: 100,
derefAliases: 0,
typesOnly: false,
attributeValues: true,
},
set: { p: 'v' },
map: {
@@ -205,7 +205,7 @@ describe('readLdapConfig', () => {
timeLimit: 42,
sizeLimit: 100,
derefAliases: 1,
typesOnly: true,
attributeValues: false,
},
set: { p: 'v' },
map: {
@@ -247,7 +247,7 @@ describe('readLdapConfig', () => {
timeLimit: 42,
sizeLimit: 100,
derefAliases: 0,
typesOnly: false,
returnAttributeValues: true,
},
set: { p: 'v' },
map: {
@@ -275,7 +275,7 @@ describe('readLdapConfig', () => {
timeLimit: 42,
sizeLimit: 100,
derefAliases: 1,
typesOnly: true,
returnAttributeValues: false,
},
set: { p: 'v' },
map: {
@@ -20,7 +20,7 @@ import {
} from '@backstage/backend-plugin-api';
import { Config } from '@backstage/config';
import { JsonValue } from '@backstage/types';
import { SearchOptions } from 'ldapjs';
import { SearchOptions } from 'ldapts';
import mergeWith from 'lodash/mergeWith';
import { trimEnd } from 'lodash';
import { RecursivePartial } from './util';
@@ -274,8 +274,10 @@ function readOptionsConfig(c: Config | undefined): SearchOptions {
attributes: c.getOptionalStringArray('attributes'),
sizeLimit: c.getOptionalNumber('sizeLimit'),
timeLimit: c.getOptionalNumber('timeLimit'),
derefAliases: c.getOptionalNumber('derefAliases'),
typesOnly: c.getOptionalBoolean('typesOnly'),
derefAliases: c.getOptionalNumber(
'derefAliases',
) as SearchOptions['derefAliases'],
returnAttributeValues: c.getOptionalBoolean('attributeValues'),
...(paged !== undefined ? { paged } : undefined),
};
}
@@ -15,7 +15,7 @@
*/
import { GroupEntity, UserEntity } from '@backstage/catalog-model';
import { SearchEntry } from 'ldapjs';
import { Entry } from 'ldapts';
import merge from 'lodash/merge';
import { LdapClient } from './client';
import { GroupConfig, UserConfig, VendorConfig } from './config';
@@ -36,6 +36,7 @@ import {
ActiveDirectoryVendor,
DefaultLdapVendor,
FreeIpaVendor,
GoogleLdapVendor,
} from './vendors';
function user(data: RecursivePartial<UserEntity>): UserEntity {
@@ -64,40 +65,32 @@ function group(data: RecursivePartial<GroupEntity>): GroupEntity {
);
}
function searchEntry(
attributes: Record<string, string[] | Buffer[]>,
): SearchEntry {
return {
raw: Object.entries(attributes).reduce((obj, [key, values]) => {
obj[key] = values;
return obj;
}, {} as any),
} as any;
}
describe('readLdapUsers', () => {
const client: jest.Mocked<LdapClient> = {
searchStreaming: jest.fn(),
search: jest.fn(),
getVendor: jest.fn(),
} as any;
afterEach(() => jest.resetAllMocks());
it('transfers all attributes from a default ldap vendor', async () => {
const searchEntries: Entry[] = [
{
dn: 'dn-value',
uid: 'uid-value',
description: 'description-value',
cn: 'cn-value',
mail: 'mail-value',
avatarUrl: 'avatarUrl-value',
memberOf: ['x', 'y', 'z'],
entryDN: 'dn-value',
entryUUID: 'uuid-value',
},
];
client.getVendor.mockResolvedValue(DefaultLdapVendor);
client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => {
await fn(
searchEntry({
uid: ['uid-value'],
description: ['description-value'],
cn: ['cn-value'],
mail: ['mail-value'],
avatarUrl: ['avatarUrl-value'],
memberOf: ['x', 'y', 'z'],
entryDN: ['dn-value'],
entryUUID: ['uuid-value'],
}),
);
client.search.mockResolvedValue({
searchEntries,
searchReferences: [],
});
const config: UserConfig[] = [
{
@@ -142,20 +135,23 @@ describe('readLdapUsers', () => {
});
it('override default vendor configs', async () => {
const searchEntries: Entry[] = [
{
dn: 'dn-vaule',
uid: 'uid-value',
description: 'description-value',
cn: 'cn-value',
mail: 'mail-value',
avatarUrl: 'avatarUrl-value',
memberOf: ['x', 'y', 'z'],
customDN: 'dn-value',
customUUID: 'uuid-value',
},
];
client.getVendor.mockResolvedValue(DefaultLdapVendor);
client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => {
await fn(
searchEntry({
uid: ['uid-value'],
description: ['description-value'],
cn: ['cn-value'],
mail: ['mail-value'],
avatarUrl: ['avatarUrl-value'],
memberOf: ['x', 'y', 'z'],
customDN: ['dn-value'],
customUUID: ['uuid-value'],
}),
);
client.search.mockResolvedValue({
searchEntries,
searchReferences: [],
});
const config: UserConfig[] = [
{
@@ -211,25 +207,23 @@ describe('readLdapUsers', () => {
});
it('should allow skipping memberOf', async () => {
const searchEntries: Entry[] = [
{
dn: 'dn-value',
uid: ['uid-value'],
description: ['description-value'],
cn: ['cn-value'],
mail: ['mail-value'],
avatarUrl: ['avatarUrl-value'],
memberOf: ['x', 'y', 'z'],
customDN: ['dn-value'],
customUUID: ['uuid-value'],
},
];
client.getVendor.mockResolvedValue(DefaultLdapVendor);
client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => {
await fn(searchEntry({ memberOf: ['x', 'y', 'z'] }));
});
client.getVendor.mockResolvedValue(DefaultLdapVendor);
client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => {
await fn(
searchEntry({
uid: ['uid-value'],
description: ['description-value'],
cn: ['cn-value'],
mail: ['mail-value'],
avatarUrl: ['avatarUrl-value'],
memberOf: ['x', 'y', 'z'],
customDN: ['dn-value'],
customUUID: ['uuid-value'],
}),
);
client.search.mockResolvedValue({
searchEntries,
searchReferences: [],
});
const config: UserConfig[] = [
{
@@ -257,26 +251,90 @@ describe('readLdapUsers', () => {
expect(userMemberOf.size).toBe(0);
});
it('transfers all attributes from Google', async () => {
const searchEntries: Entry[] = [
{
dn: 'dn-value',
uid: 'uid-value',
cn: 'cn-value',
mail: 'email-value',
description: 'description-value',
entryUuid: 'uid-value',
memberOf: ['x', 'y', 'z'],
},
];
client.getVendor.mockResolvedValue(GoogleLdapVendor);
client.search.mockResolvedValue({
searchEntries,
searchReferences: [],
});
const config: UserConfig[] = [
{
dn: 'ddd',
options: {},
map: {
rdn: 'uid',
name: 'uid',
description: 'description',
displayName: 'cn',
email: 'mail',
memberOf: 'memberOf',
},
},
];
const { users, userMemberOf } = await readLdapUsers(client, config, {});
expect(users).toEqual([
expect.objectContaining({
metadata: {
name: 'uid-value',
description: 'description-value',
annotations: {
[LDAP_DN_ANNOTATION]: 'dn-value',
[LDAP_RDN_ANNOTATION]: 'uid-value',
[LDAP_UUID_ANNOTATION]: 'uid-value',
},
},
spec: {
profile: {
displayName: 'cn-value',
email: 'email-value',
},
memberOf: [],
},
}),
]);
expect(userMemberOf).toEqual(
new Map([['dn-value', new Set(['x', 'y', 'z'])]]),
);
});
it('transfers all attributes from Microsoft Active Directory', async () => {
const searchEntries: Entry[] = [
{
dn: 'dn-value',
uid: 'uid-value',
description: ['description-value'],
cn: ['cn-value'],
mail: ['mail-value'],
avatarUrl: ['avatarUrl-value'],
memberOf: ['x', 'y', 'z'],
distinguishedName: ['dn-value'],
objectGUID: [
Buffer.from([
68, 2, 125, 190, 209, 0, 94, 73, 133, 33, 230, 174, 234, 195, 160,
152,
]),
],
},
];
client.getVendor.mockResolvedValue(ActiveDirectoryVendor);
client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => {
await fn(
searchEntry({
uid: ['uid-value'],
description: ['description-value'],
cn: ['cn-value'],
mail: ['mail-value'],
avatarUrl: ['avatarUrl-value'],
memberOf: ['x', 'y', 'z'],
distinguishedName: ['dn-value'],
objectGUID: [
Buffer.from([
68, 2, 125, 190, 209, 0, 94, 73, 133, 33, 230, 174, 234, 195, 160,
152,
]),
],
}),
);
client.search.mockResolvedValue({
searchEntries,
searchReferences: [],
});
const config: UserConfig[] = [
{
@@ -321,20 +379,22 @@ describe('readLdapUsers', () => {
});
it('transfers all attributes from FreeIPA', async () => {
const searchEntries: Entry[] = [
{
uid: 'uid-value',
description: ['description-value'],
cn: ['cn-value'],
mail: ['mail-value'],
avatarUrl: ['avatarUrl-value'],
memberOf: ['x', 'y', 'z'],
dn: 'dn-value',
ipaUniqueID: ['uuid-value'],
},
];
client.getVendor.mockResolvedValue(FreeIpaVendor);
client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => {
await fn(
searchEntry({
uid: ['uid-value'],
description: ['description-value'],
cn: ['cn-value'],
mail: ['mail-value'],
avatarUrl: ['avatarUrl-value'],
memberOf: ['x', 'y', 'z'],
dn: ['dn-value'],
ipaUniqueID: ['uuid-value'],
}),
);
client.search.mockResolvedValue({
searchEntries,
searchReferences: [],
});
const config: UserConfig[] = [
{
@@ -380,21 +440,25 @@ describe('readLdapUsers', () => {
it('transfers all attributes from for vendorDN case sensitivity', async () => {
const vendor = DefaultLdapVendor;
const searchEntries: Entry[] = [
{
dn: 'dn-VALUE',
uid: ['uid-value'],
description: ['description-value'],
cn: ['cn-value'],
mail: ['mail-value'],
avatarUrl: ['avatarUrl-value'],
memberOf: ['x', 'Y', 'z'],
entryDN: ['dn-VALUE'],
entryUUID: ['uuid-value'],
},
];
client.getVendor.mockResolvedValue(vendor);
client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => {
await fn(
searchEntry({
uid: ['uid-value'],
description: ['description-value'],
cn: ['cn-value'],
mail: ['mail-value'],
avatarUrl: ['avatarUrl-value'],
memberOf: ['x', 'Y', 'z'],
entryDN: ['dn-VALUE'],
entryUUID: ['uuid-value'],
}),
);
client.search.mockResolvedValue({
searchEntries,
searchReferences: [],
});
const config: UserConfig[] = [
{
dn: 'ddd',
@@ -436,23 +500,28 @@ describe('readLdapUsers', () => {
new Map([['dn-VALUE', new Set(['x', 'Y', 'z'])]]),
);
});
it('fails to transfer all attributes from for due case sensitivity', async () => {
const vendor = DefaultLdapVendor;
client.getVendor.mockResolvedValue(vendor);
client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => {
await fn(
searchEntry({
uid: ['uid-value'],
description: ['description-value'],
cn: ['cn-value'],
mail: ['mail-value'],
avatarUrl: ['avatarUrl-value'],
memberOf: ['x', 'Y', 'z'],
entryDN: ['dn-VALUE'],
entryUUID: ['uuid-value'],
}),
);
const searchEntries: Entry[] = [
{
dn: 'dn-VALUE',
uid: ['uid-value'],
description: ['description-value'],
cn: ['cn-value'],
mail: ['mail-value'],
avatarUrl: ['avatarUrl-value'],
memberOf: ['x', 'Y', 'z'],
entryDN: ['dn-VALUE'],
entryUUID: ['uuid-value'],
},
];
client.getVendor.mockResolvedValue(ActiveDirectoryVendor);
client.search.mockResolvedValue({
searchEntries,
searchReferences: [],
});
const config: UserConfig[] = [
{
dn: 'ddd',
@@ -498,19 +567,22 @@ describe('readLdapUsers', () => {
it('can process a list of UserConfigs', async () => {
client.getVendor.mockResolvedValue(DefaultLdapVendor);
client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => {
await fn(
searchEntry({
uid: ['uid-value'],
description: ['description-value'],
cn: ['cn-value'],
mail: ['mail-value'],
avatarUrl: ['avatarUrl-value'],
memberOf: ['x', 'y', 'z'],
entryDN: ['dn-value'],
entryUUID: ['uuid-value'],
}),
);
const searchEntries: Entry[] = [
{
dn: 'dn-value',
uid: 'uid-value',
description: 'description-value',
cn: 'cn-value',
mail: 'mail-value',
avatarUrl: 'avatarUrl-value',
memberOf: ['x', 'y', 'z'],
entryDN: 'dn-value',
entryUUID: 'uuid-value',
},
];
client.search.mockResolvedValue({
searchEntries,
searchReferences: [],
});
const config: UserConfig[] = [
{
@@ -552,7 +624,7 @@ describe('readLdapUsers', () => {
describe('readLdapGroups', () => {
const client: jest.Mocked<LdapClient> = {
searchStreaming: jest.fn(),
search: jest.fn(),
getVendor: jest.fn(),
} as any;
@@ -560,20 +632,23 @@ describe('readLdapGroups', () => {
it('transfers all attributes from a default ldap vendor', async () => {
client.getVendor.mockResolvedValue(DefaultLdapVendor);
client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => {
await fn(
searchEntry({
cn: ['cn-value'],
description: ['description-value'],
tt: ['type-value'],
mail: ['mail-value'],
avatarUrl: ['avatarUrl-value'],
memberOf: ['x', 'y', 'z'],
member: ['e', 'f', 'g'],
entryDN: ['dn-value'],
entryUUID: ['uuid-value'],
}),
);
const searchEntries: Entry[] = [
{
dn: 'dn-value',
cn: 'cn-value',
description: 'description-value',
tt: 'type-value',
mail: 'mail-value',
avatarUrl: 'avatarUrl-value',
memberOf: ['x', 'y', 'z'],
member: ['e', 'f', 'g'],
entryDN: 'dn-value',
entryUUID: 'uuid-value',
},
];
client.search.mockResolvedValue({
searchEntries,
searchReferences: [],
});
const config: GroupConfig[] = [
{
@@ -627,27 +702,94 @@ describe('readLdapGroups', () => {
);
});
it('transfers all attributes from Google', async () => {
client.getVendor.mockResolvedValue(GoogleLdapVendor);
const searchEntries: Entry[] = [
{
dn: 'dn-value',
uid: 'uid-value',
cn: 'cn-value',
mail: 'email-value',
tt: 'type-value',
description: 'description-value',
entryUuid: 'uid-value',
member: ['e', 'f', 'g'],
},
];
client.search.mockResolvedValue({
searchEntries,
searchReferences: [],
});
const config: GroupConfig[] = [
{
dn: 'ddd',
options: {},
map: {
rdn: 'uid',
name: 'uid',
description: 'description',
displayName: 'cn',
email: 'mail',
type: 'tt',
members: 'member',
memberOf: 'memberOf',
},
},
];
const { groups, groupMember, groupMemberOf } = await readLdapGroups(
client,
config,
{},
);
expect(groups).toEqual([
expect.objectContaining({
metadata: {
name: 'uid-value',
description: 'description-value',
annotations: {
[LDAP_DN_ANNOTATION]: 'dn-value',
[LDAP_RDN_ANNOTATION]: 'uid-value',
[LDAP_UUID_ANNOTATION]: 'uid-value',
},
},
spec: {
type: 'type-value',
profile: {
displayName: 'cn-value',
email: 'email-value',
},
children: [],
},
}),
]);
expect(groupMember).toEqual(
new Map([['dn-value', new Set(['e', 'f', 'g'])]]),
);
expect(groupMemberOf).toEqual(new Map([['dn-value', new Set()]]));
});
it('transfers all attributes from Microsoft Active Directory', async () => {
client.getVendor.mockResolvedValue(ActiveDirectoryVendor);
client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => {
await fn(
searchEntry({
cn: ['cn-value'],
description: ['description-value'],
tt: ['type-value'],
mail: ['mail-value'],
avatarUrl: ['avatarUrl-value'],
memberOf: ['x', 'y', 'z'],
member: ['e', 'f', 'g'],
distinguishedName: ['dn-value'],
objectGUID: [
Buffer.from([
68, 2, 125, 190, 209, 0, 94, 73, 133, 33, 230, 174, 234, 195, 160,
152,
]),
],
}),
);
const searchEntries: Entry[] = [
{
dn: 'dn-value',
cn: 'cn-value',
description: 'description-value',
tt: 'type-value',
mail: 'mail-value',
avatarUrl: 'avatarUrl-value',
memberOf: ['x', 'y', 'z'],
member: ['e', 'f', 'g'],
distinguishedName: 'dn-value',
objectGUID: Buffer.from([
68, 2, 125, 190, 209, 0, 94, 73, 133, 33, 230, 174, 234, 195, 160,
152,
]),
},
];
client.search.mockResolvedValue({
searchEntries,
searchReferences: [],
});
const config: GroupConfig[] = [
{
@@ -703,20 +845,23 @@ describe('readLdapGroups', () => {
it('override default vendor configs', async () => {
client.getVendor.mockResolvedValue(DefaultLdapVendor);
client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => {
await fn(
searchEntry({
cn: ['cn-value'],
description: ['description-value'],
tt: ['type-value'],
mail: ['mail-value'],
avatarUrl: ['avatarUrl-value'],
memberOf: ['x', 'y', 'z'],
member: ['e', 'f', 'g'],
customDN: ['dn-value'],
customUUID: ['uuid-value'],
}),
);
const searchEntries: Entry[] = [
{
dn: 'dn-value',
cn: 'cn-value',
description: 'description-value',
tt: 'type-value',
mail: 'mail-value',
avatarUrl: 'avatarUrl-value',
memberOf: ['x', 'y', 'z'],
member: ['e', 'f', 'g'],
customDN: 'dn-value',
customUUID: 'uuid-value',
},
];
client.search.mockResolvedValue({
searchEntries,
searchReferences: [],
});
const config: GroupConfig[] = [
{
@@ -778,20 +923,23 @@ describe('readLdapGroups', () => {
it('should allow skipping members', async () => {
client.getVendor.mockResolvedValue(DefaultLdapVendor);
client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => {
await fn(
searchEntry({
cn: ['cn-value'],
description: ['description-value'],
tt: ['type-value'],
mail: ['mail-value'],
avatarUrl: ['avatarUrl-value'],
memberOf: ['x', 'y', 'z'],
member: ['e', 'f', 'g'],
customDN: ['dn-value'],
customUUID: ['uuid-value'],
}),
);
const searchEntries: Entry[] = [
{
dn: 'dn-value',
cn: 'cn-value',
description: 'description-value',
tt: 'type-value',
mail: 'mail-value',
avatarUrl: 'avatarUrl-value',
memberOf: ['x', 'y', 'z'],
member: ['e', 'f', 'g'],
customDN: 'dn-value',
customUUID: 'uuid-value',
},
];
client.search.mockResolvedValue({
searchEntries,
searchReferences: [],
});
const config: GroupConfig[] = [
{
@@ -823,20 +971,23 @@ describe('readLdapGroups', () => {
it('can process a list of GroupConfigs', async () => {
client.getVendor.mockResolvedValue(DefaultLdapVendor);
client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => {
await fn(
searchEntry({
cn: ['cn-value'],
description: ['description-value'],
tt: ['type-value'],
mail: ['mail-value'],
avatarUrl: ['avatarUrl-value'],
memberOf: ['x', 'y', 'z'],
member: ['e', 'f', 'g'],
entryDN: ['dn-value'],
entryUUID: ['uuid-value'],
}),
);
const searchEntries: Entry[] = [
{
dn: 'dn-value',
cn: 'cn-value',
description: 'description-value',
tt: 'type-value',
mail: 'mail-value',
avatarUrl: 'avatarUrl-value',
memberOf: ['x', 'y', 'z'],
member: ['e', 'f', 'g'],
entryDN: 'dn-value',
entryUUID: 'uuid-value',
},
];
client.search.mockResolvedValue({
searchEntries,
searchReferences: [],
});
const config: GroupConfig[] = [
{
@@ -1026,7 +1177,8 @@ describe('defaultUserTransformer', () => {
},
};
const entry = searchEntry({
const entry: Entry = {
dn: 'dn-value',
uid: ['uid-value'],
description: ['description-value'],
cn: ['cn-value'],
@@ -1035,7 +1187,7 @@ describe('defaultUserTransformer', () => {
memberOf: ['x', 'y', 'z'],
entryDN: ['dn-value'],
entryUUID: ['uuid-value'],
});
};
let output = await defaultUserTransformer(DefaultLdapVendor, config, entry);
expect(output).toEqual({
@@ -1095,12 +1247,13 @@ describe('defaultUserTransformer', () => {
set: {},
};
const entry = searchEntry({
const entry: Entry = {
dn: 'ddd',
description: ['description-value'],
cn: ['cn-value'],
mail: ['mail-value'],
memberOf: ['x', 'y', 'z'],
});
};
await expect(
defaultUserTransformer(DefaultLdapVendor, config, entry),
@@ -1131,7 +1284,8 @@ describe('defaultGroupTransformer', () => {
},
};
const entry = searchEntry({
const entry: Entry = {
dn: 'dn-value',
uid: ['uid-value'],
description: ['description-value'],
cn: ['cn-value'],
@@ -1140,7 +1294,7 @@ describe('defaultGroupTransformer', () => {
memberOf: ['x', 'y', 'z'],
entryDN: ['dn-value'],
entryUUID: ['uuid-value'],
});
};
let output = await defaultGroupTransformer(
DefaultLdapVendor,
@@ -1210,14 +1364,15 @@ describe('defaultGroupTransformer', () => {
},
};
const entry = searchEntry({
const entry: Entry = {
dn: 'dn-value',
description: ['description-value'],
mail: ['mail-value'],
avatarUrl: ['avatarUrl-value'],
memberOf: ['x', 'y', 'z'],
entryDN: ['dn-value'],
entryUUID: ['uuid-value'],
});
};
await expect(
defaultGroupTransformer(DefaultLdapVendor, config, entry),
@@ -1248,7 +1403,8 @@ describe('defaultUserTransformerWithCaseSensitiveDNs', () => {
},
};
const entry = searchEntry({
const entry: Entry = {
dn: 'dn-VALUE',
uid: ['uid-value'],
description: ['description-value'],
cn: ['cn-value'],
@@ -1257,7 +1413,7 @@ describe('defaultUserTransformerWithCaseSensitiveDNs', () => {
memberOf: ['x', 'y', 'z'],
entryDN: ['dn-VALUE'],
entryUUID: ['uuid-value'],
});
};
const vendor = DefaultLdapVendor;
let output = await defaultUserTransformer(vendor, config, entry);
@@ -1326,7 +1482,8 @@ describe('defaultGroupTransformerWithCaseSensitiveDNs', () => {
},
};
const entry = searchEntry({
const entry: Entry = {
dn: 'dn-VALUE',
uid: ['uid-value'],
description: ['description-value'],
cn: ['cn-value'],
@@ -1335,7 +1492,7 @@ describe('defaultGroupTransformerWithCaseSensitiveDNs', () => {
memberOf: ['x', 'y', 'z'],
entryDN: ['dn-VALUE'],
entryUUID: ['uuid-value'],
});
};
const vendor = DefaultLdapVendor;
@@ -19,7 +19,7 @@ import {
stringifyEntityRef,
UserEntity,
} from '@backstage/catalog-model';
import { SearchEntry } from 'ldapjs';
import { Entry } from 'ldapts';
import lodashSet from 'lodash/set';
import cloneDeep from 'lodash/cloneDeep';
import { buildOrgHierarchy } from './org';
@@ -45,7 +45,7 @@ import { InputError } from '@backstage/errors';
export async function defaultUserTransformer(
vendor: LdapVendor,
config: UserConfig,
entry: SearchEntry,
entry: Entry,
): Promise<UserEntity | undefined> {
const { set, map } = config;
@@ -136,19 +136,20 @@ export async function readLdapUsers(
for (const cfg of userConfig) {
const { dn, options, map } = cfg;
await client.searchStreaming(dn, options, async user => {
const entity = await transformer(vendor, cfg, user);
const searchResult = await client.search(dn, options);
for (const entry of searchResult.searchEntries) {
const entity = await transformer(vendor, cfg, entry);
if (!entity) {
return;
continue;
}
mapReferencesAttr(user, vendor, map.memberOf, (myDn, vs) => {
mapReferencesAttr(entry, vendor, map.memberOf, (myDn, vs) => {
ensureItems(userMemberOf, myDn, vs);
});
entities.push(entity);
});
}
}
return { users: entities, userMemberOf };
@@ -163,7 +164,7 @@ export async function readLdapUsers(
export async function defaultGroupTransformer(
vendor: LdapVendor,
config: GroupConfig,
entry: SearchEntry,
entry: Entry,
): Promise<GroupEntity | undefined> {
const { set, map } = config;
const entity: GroupEntity = {
@@ -263,16 +264,12 @@ export async function readLdapGroups(
for (const cfg of groupConfig) {
const { dn, map, options } = cfg;
await client.searchStreaming(dn, options, async entry => {
if (!entry) {
return;
}
const searchResult = await client.search(dn, options);
for (const entry of searchResult.searchEntries) {
const entity = await transformer(vendor, cfg, entry);
if (!entity) {
return;
continue;
}
mapReferencesAttr(entry, vendor, map.memberOf, (myDn, vs) => {
@@ -284,7 +281,7 @@ export async function readLdapGroups(
});
groups.push(entity);
});
}
}
return {
@@ -349,7 +346,7 @@ export async function readLdapOrg(
// Maps a multi-valued attribute of references to other objects, to a consumer
function mapReferencesAttr(
entry: SearchEntry,
entry: Entry,
vendor: LdapVendor,
attributeName: string | undefined | null,
setter: (sourceDn: string, targets: string[]) => void,
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { GroupEntity, UserEntity } from '@backstage/catalog-model';
import { SearchEntry } from 'ldapjs';
import { Entry } from 'ldapts';
import { LdapVendor } from './vendors';
import { GroupConfig, UserConfig } from './config';
@@ -34,7 +34,7 @@ import { GroupConfig, UserConfig } from './config';
export type UserTransformer = (
vendor: LdapVendor,
config: UserConfig,
user: SearchEntry,
user: Entry,
) => Promise<UserEntity | undefined>;
/**
@@ -53,5 +53,5 @@ export type UserTransformer = (
export type GroupTransformer = (
vendor: LdapVendor,
config: GroupConfig,
group: SearchEntry,
group: Entry,
) => Promise<GroupEntity | undefined>;
@@ -1,73 +0,0 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { errorString, createOptions } from './util';
describe('errorString', () => {
it('formats', () => {
const e = { code: 1, name: 'n', message: 'm' };
expect(errorString(e)).toEqual('1 n: m');
});
});
describe('createOptions', () => {
it('should add pagePause', () => {
const options = {
filter: 'f',
paged: true,
timeLimit: 42,
sizeLimit: 100,
derefAliases: 0,
typesOnly: false,
};
expect(createOptions(options)).toEqual({
filter: 'f',
paged: { pagePause: true },
timeLimit: 42,
sizeLimit: 100,
derefAliases: 0,
typesOnly: false,
});
const options2 = {
filter: 'f',
paged: {},
timeLimit: 42,
sizeLimit: 100,
derefAliases: 0,
typesOnly: false,
};
expect(createOptions(options2)).toEqual({
filter: 'f',
paged: { pagePause: true },
timeLimit: 42,
sizeLimit: 100,
derefAliases: 0,
typesOnly: false,
});
});
it('should not add pagePause', () => {
const options = {
filter: 'f',
paged: false,
timeLimit: 42,
sizeLimit: 100,
derefAliases: 0,
typesOnly: false,
};
expect(createOptions(options)).toEqual(options);
});
});
@@ -14,19 +14,9 @@
* limitations under the License.
*/
import { Error as LDAPError, SearchEntry, SearchOptions } from 'ldapjs';
import { cloneDeep } from 'lodash';
import { Entry } from 'ldapts';
import { LdapVendor } from './vendors';
/**
* Builds a string form of an LDAP Error structure.
*
* @param error - The error
*/
export function errorString(error: LDAPError) {
return `${error.code} ${error.name}: ${error.message}`;
}
/**
* Maps a single-valued attribute to a consumer.
*
@@ -42,7 +32,7 @@ export function errorString(error: LDAPError) {
* @public
*/
export function mapStringAttr(
entry: SearchEntry,
entry: Entry,
vendor: LdapVendor,
attributeName: string | undefined,
setter: (value: string) => void,
@@ -55,18 +45,6 @@ export function mapStringAttr(
}
}
export function createOptions(inputOptions: SearchOptions): SearchOptions {
const result = cloneDeep(inputOptions);
if (result.paged === true) {
result.paged = { pagePause: true };
} else if (typeof result.paged === 'object') {
result.paged.pagePause = true;
}
return result;
}
export type RecursivePartial<T> = {
[P in keyof T]?: T[P] extends (infer U)[]
? RecursivePartial<U>[]
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { SearchEntry } from 'ldapjs';
import { Entry } from 'ldapts';
/**
* An LDAP Vendor handles unique nuances between different vendors.
@@ -37,7 +37,7 @@ export type LdapVendor = {
* @param entry - The ldap entry
* @param name - The attribute to decode
*/
decodeStringAttribute: (entry: SearchEntry, name: string) => string[];
decodeStringAttribute: (entry: Entry, name: string) => string[];
};
export const DefaultLdapVendor: LdapVendor = {
@@ -106,11 +106,11 @@ export const LLDAPVendor: LdapVendor = {
// Decode an attribute to a consumer
function decode(
entry: SearchEntry,
entry: Entry,
attributeName: string,
decoder: (value: string | Buffer) => string,
): string[] {
const values = entry.raw[attributeName];
const values = entry[attributeName];
if (Array.isArray(values)) {
return values.map(v => {
return decoder(v);
+56 -118
View File
@@ -5023,9 +5023,8 @@ __metadata:
"@backstage/plugin-catalog-common": "workspace:^"
"@backstage/plugin-catalog-node": "workspace:^"
"@backstage/types": "workspace:^"
"@types/ldapjs": "npm:^2.2.5"
"@types/lodash": "npm:^4.14.151"
ldapjs: "npm:^2.3.3"
ldapts: "npm:^8.0.6"
lodash: "npm:^4.17.21"
uuid: "npm:^11.0.0"
languageName: unknown
@@ -20313,6 +20312,15 @@ __metadata:
languageName: node
linkType: hard
"@types/asn1@npm:>=0.2.4":
version: 0.2.4
resolution: "@types/asn1@npm:0.2.4"
dependencies:
"@types/node": "npm:*"
checksum: 10/01fee5a896a650f92c95556c83678d66677060b5aba7936b714b5c8ca2945cc8cabf57441e323f1800a783a55958b6230d109e988ca45e83b2d7a0e071c24069
languageName: node
linkType: hard
"@types/aws-lambda@npm:8.10.150, @types/aws-lambda@npm:^8.10.83":
version: 8.10.150
resolution: "@types/aws-lambda@npm:8.10.150"
@@ -21124,15 +21132,6 @@ __metadata:
languageName: node
linkType: hard
"@types/ldapjs@npm:^2.2.5":
version: 2.2.5
resolution: "@types/ldapjs@npm:2.2.5"
dependencies:
"@types/node": "npm:*"
checksum: 10/6981e24944c8132eff6bf76d367a18aa34238fcb86c385b4711c577d62f087d770aa81f6975beddcca9f69c8ce002de73922e1f3689e1315f4ad9401edc54dcd
languageName: node
linkType: hard
"@types/libsodium-wrappers@npm:^0.7.10":
version: 0.7.14
resolution: "@types/libsodium-wrappers@npm:0.7.14"
@@ -23854,13 +23853,6 @@ __metadata:
languageName: node
linkType: hard
"abstract-logging@npm:^2.0.0":
version: 2.0.0
resolution: "abstract-logging@npm:2.0.0"
checksum: 10/2f3e0ef758a18d98ddfd4fc01532724d6ada0945e2f679a0ad3b73fbe4b3594b1a3047dafb25bb88d3cb4031c4ea6090b25f8f5b7cfb4d2c7bcf125a56aca6df
languageName: node
linkType: hard
"accepts@npm:^1.3.5, accepts@npm:^1.3.7, accepts@npm:~1.3.4, accepts@npm:~1.3.8":
version: 1.3.8
resolution: "accepts@npm:1.3.8"
@@ -24635,7 +24627,7 @@ __metadata:
languageName: node
linkType: hard
"asn1@npm:^0.2.4, asn1@npm:^0.2.6":
"asn1@npm:0.2.6, asn1@npm:^0.2.6":
version: 0.2.6
resolution: "asn1@npm:0.2.6"
dependencies:
@@ -24644,13 +24636,6 @@ __metadata:
languageName: node
linkType: hard
"assert-plus@npm:^1.0.0":
version: 1.0.0
resolution: "assert-plus@npm:1.0.0"
checksum: 10/f4f991ae2df849cc678b1afba52d512a7cbf0d09613ba111e72255409ff9158550c775162a47b12d015d1b82b3c273e8e25df0e4783d3ddb008a293486d00a07
languageName: node
linkType: hard
"assert@npm:^2.0.0":
version: 2.1.0
resolution: "assert@npm:2.1.0"
@@ -25108,15 +25093,6 @@ __metadata:
languageName: node
linkType: hard
"backoff@npm:^2.5.0":
version: 2.5.0
resolution: "backoff@npm:2.5.0"
dependencies:
precond: "npm:0.2"
checksum: 10/5286c3f02665f3347591e04728ba0755e76a45aa40e037f7db3f2029ede927bfe94755a03033e93cc971a4b6c6605d8cfe514433e362c5a86c0b5bbc5d47acce
languageName: node
linkType: hard
"bail@npm:^2.0.0":
version: 2.0.1
resolution: "bail@npm:2.0.1"
@@ -27264,7 +27240,7 @@ __metadata:
languageName: node
linkType: hard
"core-util-is@npm:1.0.2, core-util-is@npm:~1.0.0":
"core-util-is@npm:~1.0.0":
version: 1.0.2
resolution: "core-util-is@npm:1.0.2"
checksum: 10/d0f7587346b44a1fe6c269267e037dd34b4787191e473c3e685f507229d88561c40eb18872fabfff02977301815d474300b7bfbd15396c13c5377393f7e87ec3
@@ -28114,15 +28090,15 @@ __metadata:
languageName: node
linkType: hard
"debug@npm:4, debug@npm:^4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.3.6, debug@npm:^4.4.0":
version: 4.4.0
resolution: "debug@npm:4.4.0"
"debug@npm:4, debug@npm:4.4.1, debug@npm:^4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.3.6, debug@npm:^4.4.0":
version: 4.4.1
resolution: "debug@npm:4.4.1"
dependencies:
ms: "npm:^2.1.3"
peerDependenciesMeta:
supports-color:
optional: true
checksum: 10/1847944c2e3c2c732514b93d11886575625686056cd765336212dc15de2d2b29612b6cd80e1afba767bb8e1803b778caf9973e98169ef1a24a7a7009e1820367
checksum: 10/8e2709b2144f03c7950f8804d01ccb3786373df01e406a0f66928e47001cf2d336cbed9ee137261d4f90d68d8679468c755e3548ed83ddacdc82b194d2468afe
languageName: node
linkType: hard
@@ -30865,13 +30841,6 @@ __metadata:
languageName: node
linkType: hard
"extsprintf@npm:^1.2.0":
version: 1.4.0
resolution: "extsprintf@npm:1.4.0"
checksum: 10/c1e6cc79d7efc23770b3688bac3b8ec1f0200bca18c2a5e4e2697f9b9d4b9b1f2e5439541437fe90923bbd1afbeb9507cd68b10832e14ca475a9354b990872c3
languageName: node
linkType: hard
"fast-copy@npm:^3.0.2":
version: 3.0.2
resolution: "fast-copy@npm:3.0.2"
@@ -36491,28 +36460,17 @@ __metadata:
languageName: node
linkType: hard
"ldap-filter@npm:^0.3.3":
version: 0.3.3
resolution: "ldap-filter@npm:0.3.3"
"ldapts@npm:^8.0.6":
version: 8.0.6
resolution: "ldapts@npm:8.0.6"
dependencies:
assert-plus: "npm:^1.0.0"
checksum: 10/9d34e812a16e328e53ceef7821fd7aa143492d1c3accd1976ef38ec2864344e82c3de812f49ac59f083d1f7a9aa956a2831f75c5ec72189f729bd495b2a513ee
languageName: node
linkType: hard
"ldapjs@npm:^2.3.3":
version: 2.3.3
resolution: "ldapjs@npm:2.3.3"
dependencies:
abstract-logging: "npm:^2.0.0"
asn1: "npm:^0.2.4"
assert-plus: "npm:^1.0.0"
backoff: "npm:^2.5.0"
ldap-filter: "npm:^0.3.3"
once: "npm:^1.4.0"
vasync: "npm:^2.2.0"
verror: "npm:^1.8.1"
checksum: 10/98ef566afe83bc4e138b200e4edff1aa8558cb3b0cfb806f772a4fdb85df49c694607ca725993f34a399d06e14e9461d3a39ca782d84b22eebff5d1fbaaf5fbc
"@types/asn1": "npm:>=0.2.4"
asn1: "npm:0.2.6"
debug: "npm:4.4.1"
strict-event-emitter-types: "npm:2.0.0"
uuid: "npm:11.1.0"
whatwg-url: "npm:14.2.0"
checksum: 10/583fdf70762858fab1fc660670f482aa146481b4247b6e34eebe56628c7c069fe4167584c4159b3998503de18a716cb845e02b28794bc783de9b8c09834e4b15
languageName: node
linkType: hard
@@ -42301,13 +42259,6 @@ __metadata:
languageName: node
linkType: hard
"precond@npm:0.2":
version: 0.2.3
resolution: "precond@npm:0.2.3"
checksum: 10/d5215e17cc812996f72ee57a684ff159709bdfa48538e71c361d17aecd750bf25f64f85ba2c49031860708e0e70ac4394b9c8280725f227b88bce0fe76f8389a
languageName: node
linkType: hard
"preferred-pm@npm:^3.0.3":
version: 3.0.3
resolution: "preferred-pm@npm:3.0.3"
@@ -46556,6 +46507,13 @@ __metadata:
languageName: node
linkType: hard
"strict-event-emitter-types@npm:2.0.0":
version: 2.0.0
resolution: "strict-event-emitter-types@npm:2.0.0"
checksum: 10/d7b28708bf09648302e8ea5a6c4999d7e3186a06f68568a6196a71e315bf4ef44f78011db11576b0b4ed45df4a8f0baf0d64232d2a6eaf21fa92c7ec4085f481
languageName: node
linkType: hard
"strict-event-emitter@npm:^0.2.4":
version: 0.2.8
resolution: "strict-event-emitter@npm:0.2.8"
@@ -47883,12 +47841,12 @@ __metadata:
languageName: node
linkType: hard
"tr46@npm:^5.0.0":
version: 5.0.0
resolution: "tr46@npm:5.0.0"
"tr46@npm:^5.1.0":
version: 5.1.1
resolution: "tr46@npm:5.1.1"
dependencies:
punycode: "npm:^2.3.1"
checksum: 10/29155adb167d048d3c95d181f7cb5ac71948b4e8f3070ec455986e1f34634acae50ae02a3c8d448121c3afe35b76951cd46ed4c128fd80264280ca9502237a3e
checksum: 10/833a0e1044574da5790148fd17866d4ddaea89e022de50279967bcd6b28b4ce0d30d59eb3acf9702b60918975b3bad481400337e3a2e6326cffa5c77b874753d
languageName: node
linkType: hard
@@ -49262,6 +49220,15 @@ __metadata:
languageName: node
linkType: hard
"uuid@npm:11.1.0, uuid@npm:^11.0.0, uuid@npm:^11.0.2, uuid@npm:^11.0.3":
version: 11.1.0
resolution: "uuid@npm:11.1.0"
bin:
uuid: dist/esm/bin/uuid
checksum: 10/d2da43b49b154d154574891ced66d0c83fc70caaad87e043400cf644423b067542d6f3eb641b7c819224a7cd3b4c2f21906acbedd6ec9c6a05887aa9115a9cf5
languageName: node
linkType: hard
"uuid@npm:8.3.2, uuid@npm:^8.0.0, uuid@npm:^8.3.0, uuid@npm:^8.3.2":
version: 8.3.2
resolution: "uuid@npm:8.3.2"
@@ -49280,15 +49247,6 @@ __metadata:
languageName: node
linkType: hard
"uuid@npm:^11.0.0, uuid@npm:^11.0.2, uuid@npm:^11.0.3":
version: 11.1.0
resolution: "uuid@npm:11.1.0"
bin:
uuid: dist/esm/bin/uuid
checksum: 10/d2da43b49b154d154574891ced66d0c83fc70caaad87e043400cf644423b067542d6f3eb641b7c819224a7cd3b4c2f21906acbedd6ec9c6a05887aa9115a9cf5
languageName: node
linkType: hard
"uuid@npm:^3.4.0":
version: 3.4.0
resolution: "uuid@npm:3.4.0"
@@ -49416,26 +49374,6 @@ __metadata:
languageName: node
linkType: hard
"vasync@npm:^2.2.0":
version: 2.2.0
resolution: "vasync@npm:2.2.0"
dependencies:
verror: "npm:1.10.0"
checksum: 10/192a15eb5caf22af5b2ec578400a6b00ef22230575df11e815144da49170841fedd3a5af96a2a62f8fc5ec5e60ce67e8ae311b509a95043e55081665c464cf7b
languageName: node
linkType: hard
"verror@npm:1.10.0, verror@npm:^1.8.1":
version: 1.10.0
resolution: "verror@npm:1.10.0"
dependencies:
assert-plus: "npm:^1.0.0"
core-util-is: "npm:1.0.2"
extsprintf: "npm:^1.2.0"
checksum: 10/da548149dd9c130a8a2587c9ee71ea30128d1526925707e2d01ed9c5c45c9e9f86733c66a328247cdd5f7c1516fb25b0f959ba754bfbe15072aa99ff96468a29
languageName: node
linkType: hard
"vfile-message@npm:^3.0.0":
version: 3.0.2
resolution: "vfile-message@npm:3.0.2"
@@ -49906,6 +49844,16 @@ __metadata:
languageName: node
linkType: hard
"whatwg-url@npm:14.2.0, whatwg-url@npm:^14.0.0":
version: 14.2.0
resolution: "whatwg-url@npm:14.2.0"
dependencies:
tr46: "npm:^5.1.0"
webidl-conversions: "npm:^7.0.0"
checksum: 10/f0a95b0601c64f417c471536a2d828b4c16fe37c13662483a32f02f183ed0f441616609b0663fb791e524e8cd56d9a86dd7366b1fc5356048ccb09b576495e7c
languageName: node
linkType: hard
"whatwg-url@npm:^11.0.0":
version: 11.0.0
resolution: "whatwg-url@npm:11.0.0"
@@ -49916,16 +49864,6 @@ __metadata:
languageName: node
linkType: hard
"whatwg-url@npm:^14.0.0":
version: 14.0.0
resolution: "whatwg-url@npm:14.0.0"
dependencies:
tr46: "npm:^5.0.0"
webidl-conversions: "npm:^7.0.0"
checksum: 10/67ea7a359a90663b28c816d76379b4be62d13446e9a4c0ae0b5ae0294b1c22577750fcdceb40827bb35a61777b7093056953c856604a28b37d6a209ba59ad062
languageName: node
linkType: hard
"whatwg-url@npm:^5.0.0":
version: 5.0.0
resolution: "whatwg-url@npm:5.0.0"