Merge pull request #30594 from ganievs/feature/move-to-ldapts
Feature/move to ldapts
This commit is contained in:
@@ -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
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user