Merge pull request #24909 from dweber019/feat/migrate-ldap
Migrate LDAP catalog module to the new backend system
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend-module-ldap': minor
|
||||
---
|
||||
|
||||
Migrate LDAP catalog module to the new backend system.
|
||||
@@ -1369,7 +1369,7 @@ The vast majority of the backend plugins that currently live in the Backstage Re
|
||||
| @backstage/plugin-catalog-backend-module-github-org | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-github-org/README.md) |
|
||||
| @backstage/plugin-catalog-backend-module-gitlab | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-gitlab/README.md) |
|
||||
| @backstage/plugin-catalog-backend-module-incremental-ingestion | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-incremental-ingestion/README.md) |
|
||||
| @backstage/plugin-catalog-backend-module-ldap | backend-plugin-module | | | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-ldap/README.md) |
|
||||
| @backstage/plugin-catalog-backend-module-ldap | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-ldap/README.md) |
|
||||
| @backstage/plugin-catalog-backend-module-msgraph | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-msgraph/README.md) |
|
||||
| @backstage/plugin-catalog-backend-module-openapi | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-openapi/README.md) |
|
||||
| @backstage/plugin-catalog-backend-module-puppetdb | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-puppetdb/README.md) |
|
||||
|
||||
@@ -0,0 +1,417 @@
|
||||
---
|
||||
id: org--old
|
||||
title: LDAP Organizational Data
|
||||
sidebar_label: Org Data
|
||||
# prettier-ignore
|
||||
description: Setting up ingestion of organizational data from LDAP
|
||||
---
|
||||
|
||||
The Backstage catalog can be set up to ingest organizational data - users and
|
||||
groups - directly from an LDAP compatible service. The result is a hierarchy of
|
||||
[`User`](../../features/software-catalog/descriptor-format.md#kind-user) and
|
||||
[`Group`](../../features/software-catalog/descriptor-format.md#kind-group) kind
|
||||
entities that mirror your org setup.
|
||||
|
||||
## Supported vendors
|
||||
|
||||
Backstage in general supports OpenLDAP compatible vendors, as well as Active Directory and FreeIPA. If you are using a vendor that does not seem to be supported, please [file an issue](https://github.com/backstage/backstage/issues/new?assignees=&labels=enhancement&template=feature_template.md).
|
||||
|
||||
## Installation
|
||||
|
||||
This guide will use the Entity Provider method. If you for some reason prefer
|
||||
the Processor method (not recommended), it is described separately below.
|
||||
|
||||
The provider is not installed by default, therefore you have to add a dependency
|
||||
to `@backstage/plugin-catalog-backend-module-ldap` to your backend package.
|
||||
|
||||
```bash
|
||||
# From your Backstage root directory
|
||||
yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-ldap
|
||||
```
|
||||
|
||||
:::note Note
|
||||
|
||||
When configuring to use a Provider instead of a Processor you do not
|
||||
need to add a _location_ pointing to your LDAP server
|
||||
|
||||
:::
|
||||
|
||||
Update the catalog plugin initialization in your backend to add the provider and
|
||||
schedule it:
|
||||
|
||||
```ts title="packages/backend/src/plugins/catalog.ts"
|
||||
/* highlight-add-next-line */
|
||||
import { LdapOrgEntityProvider } from '@backstage/plugin-catalog-backend-module-ldap';
|
||||
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
const builder = await CatalogBuilder.create(env);
|
||||
|
||||
/* highlight-add-start */
|
||||
// The target parameter below needs to match the ldap.providers.target
|
||||
// value specified in your app-config.
|
||||
builder.addEntityProvider(
|
||||
LdapOrgEntityProvider.fromConfig(env.config, {
|
||||
id: 'our-ldap-master',
|
||||
target: 'ldaps://ds.example.net',
|
||||
logger: env.logger,
|
||||
schedule: env.scheduler.createScheduledTaskRunner({
|
||||
frequency: { minutes: 60 },
|
||||
timeout: { minutes: 15 },
|
||||
}),
|
||||
}),
|
||||
);
|
||||
/* highlight-add-end */
|
||||
|
||||
// ..
|
||||
}
|
||||
```
|
||||
|
||||
After this, you also have to add some configuration in your app-config that
|
||||
describes what you want to import for that target.
|
||||
|
||||
## Configuration
|
||||
|
||||
The following configuration is a small example of how a setup could look for
|
||||
importing groups and users from a corporate LDAP server.
|
||||
|
||||
```yaml
|
||||
ldap:
|
||||
providers:
|
||||
- target: ldaps://ds.example.net
|
||||
bind:
|
||||
dn: uid=ldap-reader-user,ou=people,ou=example,dc=example,dc=net
|
||||
secret: ${LDAP_SECRET}
|
||||
users:
|
||||
dn: ou=people,ou=example,dc=example,dc=net
|
||||
options:
|
||||
filter: (uid=*)
|
||||
map:
|
||||
description: l
|
||||
set:
|
||||
metadata.customField: 'hello'
|
||||
groups:
|
||||
dn: ou=access,ou=groups,ou=example,dc=example,dc=net
|
||||
options:
|
||||
filter: (&(objectClass=some-group-class)(!(groupType=email)))
|
||||
map:
|
||||
description: l
|
||||
set:
|
||||
metadata.customField: 'hello'
|
||||
```
|
||||
|
||||
There may be many providers, each targeting a specific `target` which is
|
||||
supposed to match the `target` of a dedicated provider instance - i.e., you will
|
||||
add one entity provider class instance per target to ingest from.
|
||||
|
||||
These config blocks have a lot of options in them, so we will describe each
|
||||
"root" key within the block separately.
|
||||
|
||||
### target
|
||||
|
||||
This is the URL of the targeted server, typically on the form
|
||||
`ldaps://ds.example.net` for SSL enabled servers or `ldap://ds.example.net`
|
||||
without SSL.
|
||||
|
||||
#### target.tls.keys
|
||||
|
||||
`keys` in TLS options specifies location of a file, that contains private keys
|
||||
to establish connection with your LDAP server, in PEM format. See an example
|
||||
for Google Secure LDAP Service below.
|
||||
|
||||
#### target.tls.certs
|
||||
|
||||
`certs` in TLS options specifies location of a file, that contains certificate
|
||||
chains to establish connection with your LDAP server, in PEM format. See an
|
||||
example for Google Secure LDAP Service below.
|
||||
|
||||
### bind
|
||||
|
||||
The bind block specifies how the plugin should bind (essentially, to
|
||||
authenticate) towards the server. It has the following fields.
|
||||
|
||||
```yaml
|
||||
dn: uid=ldap-reader-user,ou=people,ou=example,dc=example,dc=net
|
||||
secret: ${LDAP_SECRET}
|
||||
```
|
||||
|
||||
The `dn` is the full LDAP Distinguished Name for the user that the plugin
|
||||
authenticates itself as. At this point, only regular user based authentication
|
||||
is supported.
|
||||
|
||||
The `secret` is the password of the same user. In this example, it is given in
|
||||
the form of an environment variable `LDAP_SECRET`, that has to be set when the
|
||||
backend starts.
|
||||
|
||||
### users
|
||||
|
||||
The `users` block defines the settings that govern the reading and
|
||||
interpretation of users. Its fields are explained in separate sections below.
|
||||
|
||||
#### users.dn
|
||||
|
||||
The DN under which users are stored, e.g.
|
||||
`ou=people,ou=example,dc=example,dc=net`.
|
||||
|
||||
#### users.options
|
||||
|
||||
The search options to use when sending the query to the server, when reading all
|
||||
users. All the options are shown below, with their default values, but they are
|
||||
all optional.
|
||||
|
||||
```yaml
|
||||
options:
|
||||
# One of 'base', 'one', or 'sub'.
|
||||
scope: one
|
||||
# The filter is the one that you commonly will want to specify explicitly. It
|
||||
# is a string on the standard LDAP query format. Use it to select out the set
|
||||
# of users that are of actual interest to ingest. For example, you may want
|
||||
# to filter out disabled users.
|
||||
filter: (uid=*)
|
||||
# The attribute selectors for each item, as passed to the LDAP server.
|
||||
attributes: ['*', '+']
|
||||
# This field is either 'false' to disable paging when reading from the
|
||||
# server, or an object on the form '{ pageSize: 100, pagePause: true }' that
|
||||
# specifies the details of how the paging shall work.
|
||||
paged: false
|
||||
```
|
||||
|
||||
#### users.set
|
||||
|
||||
This optional piece lets you specify a number of JSON paths (on a.b.c form) and
|
||||
hard coded values to set on those paths. This can be useful for example if you
|
||||
want to hard code a namespace or similar on the generated entities.
|
||||
|
||||
```yaml
|
||||
set:
|
||||
# Just an example; the key and value can be anything
|
||||
metadata.namespace: 'ldap'
|
||||
```
|
||||
|
||||
#### users.map
|
||||
|
||||
Mappings from well known entity fields, to LDAP attribute names. This is where
|
||||
you are able to define how to interpret the attributes of each LDAP result item,
|
||||
and to move them into the corresponding entity fields. All the options are shown
|
||||
below, with their default values, but they are all optional.
|
||||
|
||||
If you leave out an optional mapping, it will still be copied using that default
|
||||
value. For example, even if you do not put in the field `displayName` in your
|
||||
config, the provider will still copy the attribute `cn` into the entity field
|
||||
`spec.profile.displayName`.
|
||||
|
||||
```yaml
|
||||
map:
|
||||
# The name of the attribute that holds the relative
|
||||
# distinguished name of each entry.
|
||||
rdn: uid
|
||||
# The name of the attribute that shall be used for the value of
|
||||
# the metadata.name field of the entity.
|
||||
name: uid
|
||||
# The name of the attribute that shall be used for the value of
|
||||
# the metadata.description field of the entity.
|
||||
description: description
|
||||
# The name of the attribute that shall be used for the value of
|
||||
# the spec.profile.displayName field of the entity.
|
||||
displayName: cn
|
||||
# The name of the attribute that shall be used for the value of
|
||||
# the spec.profile.email field of the entity.
|
||||
email: mail
|
||||
# The name of the attribute that shall be used for the value of
|
||||
# the spec.profile.picture field of the entity.
|
||||
picture: <nothing, left out>
|
||||
# The name of the attribute that shall be used for the values of
|
||||
# the spec.memberOf field of the entity.
|
||||
memberOf: memberOf
|
||||
```
|
||||
|
||||
### groups
|
||||
|
||||
The `groups` block defines the settings that govern the reading and
|
||||
interpretation of groups. Its fields are explained in separate sections below.
|
||||
|
||||
#### groups.dn
|
||||
|
||||
The DN under which groups are stored, e.g.
|
||||
`ou=people,ou=example,dc=example,dc=net`.
|
||||
|
||||
#### groups.options
|
||||
|
||||
The search options to use when sending the query to the server, when reading all
|
||||
groups. All the options are shown below, with their default values, but they are
|
||||
all optional.
|
||||
|
||||
```yaml
|
||||
options:
|
||||
# One of 'base', 'one', or 'sub'.
|
||||
scope: one
|
||||
# The filter is the one that you commonly will want to specify explicitly. It
|
||||
# is a string on the standard LDAP query format. Use it to select out the set
|
||||
# of groups that are of actual interest to ingest. For example, you may want
|
||||
# to filter out disabled groups.
|
||||
filter: (&(objectClass=some-group-class)(!(groupType=email)))
|
||||
# The attribute selectors for each item, as passed to the LDAP server.
|
||||
attributes: ['*', '+']
|
||||
# This field is either 'false' to disable paging when reading from the
|
||||
# server, or an object on the form '{ pageSize: 100, pagePause: true }' that
|
||||
# specifies the details of how the paging shall work.
|
||||
paged: false
|
||||
```
|
||||
|
||||
#### groups.set
|
||||
|
||||
This optional piece lets you specify a number of JSON paths (on a.b.c form) and
|
||||
hard coded values to set on those paths. This can be useful for example if you
|
||||
want to hard code a namespace or similar on the generated entities.
|
||||
|
||||
```yaml
|
||||
set:
|
||||
# Just an example; the key and value can be anything
|
||||
metadata.namespace: 'ldap'
|
||||
```
|
||||
|
||||
#### groups.map
|
||||
|
||||
Mappings from well known entity fields, to LDAP attribute names. This is where
|
||||
you are able to define how to interpret the attributes of each LDAP result item,
|
||||
and to move them into the corresponding entity fields. All of the options are
|
||||
shown below, with their default values, but they are all optional.
|
||||
|
||||
If you leave out an optional mapping, it will still be copied using that default
|
||||
value. For example, even if you do not put in the field `displayName` in your
|
||||
config, the provider will still copy the attribute `cn` into the entity field
|
||||
`spec.profile.displayName`. If the target field is optional, such as the display
|
||||
name, the importer will accept missing attributes and just leave the target
|
||||
field unset. If the target field is mandatory, such as the name of the entity,
|
||||
validation will fail if the source attribute is missing.
|
||||
|
||||
```yaml
|
||||
map:
|
||||
# The name of the attribute that holds the relative
|
||||
# distinguished name of each entry. This value is copied into a
|
||||
# well known annotation to be able to query by it later.
|
||||
rdn: cn
|
||||
# The name of the attribute that shall be used for the value of
|
||||
# the metadata.name field of the entity.
|
||||
name: cn
|
||||
# The name of the attribute that shall be used for the value of
|
||||
# the metadata.description field of the entity.
|
||||
description: description
|
||||
# The name of the attribute that shall be used for the value of
|
||||
# the spec.type field of the entity.
|
||||
type: groupType
|
||||
# The name of the attribute that shall be used for the value of
|
||||
# the spec.profile.displayName field of the entity.
|
||||
displayName: cn
|
||||
# The name of the attribute that shall be used for the value of
|
||||
# the spec.profile.email field of the entity.
|
||||
email: <nothing, left out>
|
||||
# The name of the attribute that shall be used for the value of
|
||||
# the spec.profile.picture field of the entity.
|
||||
picture: <nothing, left out>
|
||||
# The name of the attribute that shall be used for the values of
|
||||
# the spec.parent field of the entity.
|
||||
memberOf: memberOf
|
||||
# The name of the attribute that shall be used for the values of
|
||||
# the spec.children field of the entity.
|
||||
members: member
|
||||
```
|
||||
|
||||
## Customize the Provider
|
||||
|
||||
In case you want to customize the ingested entities, the provider allows to pass
|
||||
transformers for users and groups. Here we will show an example of overriding
|
||||
the group transformer.
|
||||
|
||||
1. Create a transformer:
|
||||
|
||||
```ts
|
||||
export async function myGroupTransformer(
|
||||
vendor: LdapVendor,
|
||||
config: GroupConfig,
|
||||
group: SearchEntry,
|
||||
): Promise<GroupEntity | undefined> {
|
||||
// Transformations may change namespace, change entity naming pattern, fill
|
||||
// profile with more or other details...
|
||||
|
||||
// Create the group entity on your own, or wrap the default transformer
|
||||
return await defaultGroupTransformer(vendor, config, group);
|
||||
}
|
||||
```
|
||||
|
||||
2. Configure the provider with the transformer:
|
||||
|
||||
```ts
|
||||
const ldapEntityProvider = LdapOrgEntityProvider.fromConfig(env.config, {
|
||||
id: 'our-ldap-master',
|
||||
target: 'ldaps://ds.example.net',
|
||||
logger: env.logger,
|
||||
groupTransformer: myGroupTransformer,
|
||||
});
|
||||
```
|
||||
|
||||
## Using a Processor instead of a Provider
|
||||
|
||||
An alternative to using the Provider for ingesting LDAP entries is to use a
|
||||
Processor. This is the old way that's based on registering locations with the
|
||||
proper type and target, triggering the processor to run.
|
||||
|
||||
The drawback of this method is that it will leave orphaned Group/User entities
|
||||
whenever they are deleted on your LDAP server, and you cannot control the
|
||||
frequency with which they are refreshed, separately from other processors.
|
||||
|
||||
### Processor Installation
|
||||
|
||||
The `LdapOrgReaderProcessor` is not registered by default, so you have to
|
||||
register it in the catalog plugin:
|
||||
|
||||
```typescript title="packages/backend/src/plugins/catalog.ts"
|
||||
builder.addProcessor(
|
||||
LdapOrgReaderProcessor.fromConfig(env.config, {
|
||||
logger: env.logger,
|
||||
}),
|
||||
);
|
||||
```
|
||||
|
||||
### Driving LDAP Org Processor Ingestion with Locations
|
||||
|
||||
Locations point out the specific org(s) you want to import. The `type` of these
|
||||
locations must be `ldap-org`, and the `target` must point to the exact URL
|
||||
(starting with `ldap://` or `ldaps://`) of the targeted LDAP server. You can
|
||||
have several such location entries if you want, but typically you will have just
|
||||
one.
|
||||
|
||||
```yaml
|
||||
catalog:
|
||||
locations:
|
||||
- type: ldap-org
|
||||
target: ldaps://ds.example.net
|
||||
rules:
|
||||
- allow: [User, Group]
|
||||
```
|
||||
|
||||
### Example configurations
|
||||
|
||||
#### Google Secure LDAP Service
|
||||
|
||||
To sync Google Workspace/Cloud Identity organization data to users and groups in backstage,
|
||||
you must [configure Secure LDAP Service](https://support.google.com/a/answer/9048516) first.
|
||||
|
||||
Once Secure LDAP Service is configured, you can enable TLS options in LDAP configuration,
|
||||
as mentioned below. `keys` and `certs` specify the location of files that are generated
|
||||
while configuring Secure LDAP Service above.
|
||||
|
||||
```yaml
|
||||
ldap:
|
||||
providers:
|
||||
- target: ldaps://ldap.google.com:636
|
||||
tls:
|
||||
rejectUnauthorized: false
|
||||
keys: '/var/secrets/tls/gldap.key'
|
||||
certs: '/var/secrets/tls/gldap.crt'
|
||||
users:
|
||||
# users configuration comes here
|
||||
groups:
|
||||
# groups configuration comes here
|
||||
```
|
||||
+69
-154
@@ -18,9 +18,6 @@ Backstage in general supports OpenLDAP compatible vendors, as well as Active Dir
|
||||
|
||||
## Installation
|
||||
|
||||
This guide will use the Entity Provider method. If you for some reason prefer
|
||||
the Processor method (not recommended), it is described separately below.
|
||||
|
||||
The provider is not installed by default, therefore you have to add a dependency
|
||||
to `@backstage/plugin-catalog-backend-module-ldap` to your backend package.
|
||||
|
||||
@@ -29,47 +26,30 @@ to `@backstage/plugin-catalog-backend-module-ldap` to your backend package.
|
||||
yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-ldap
|
||||
```
|
||||
|
||||
:::note Note
|
||||
Next add the basic configuration to `app-config.yaml`
|
||||
|
||||
When configuring to use a Provider instead of a Processor you do not
|
||||
need to add a _location_ pointing to your LDAP server
|
||||
|
||||
:::
|
||||
|
||||
Update the catalog plugin initialization in your backend to add the provider and
|
||||
schedule it:
|
||||
|
||||
```ts title="packages/backend/src/plugins/catalog.ts"
|
||||
/* highlight-add-next-line */
|
||||
import { LdapOrgEntityProvider } from '@backstage/plugin-catalog-backend-module-ldap';
|
||||
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
const builder = await CatalogBuilder.create(env);
|
||||
|
||||
/* highlight-add-start */
|
||||
// The target parameter below needs to match the ldap.providers.target
|
||||
// value specified in your app-config.
|
||||
builder.addEntityProvider(
|
||||
LdapOrgEntityProvider.fromConfig(env.config, {
|
||||
id: 'our-ldap-master',
|
||||
target: 'ldaps://ds.example.net',
|
||||
logger: env.logger,
|
||||
schedule: env.scheduler.createScheduledTaskRunner({
|
||||
frequency: { minutes: 60 },
|
||||
timeout: { minutes: 15 },
|
||||
}),
|
||||
}),
|
||||
);
|
||||
/* highlight-add-end */
|
||||
|
||||
// ..
|
||||
}
|
||||
```yaml title="app-config.yaml"
|
||||
catalog:
|
||||
providers:
|
||||
ldapOrg:
|
||||
default:
|
||||
target: ldaps://ds.example.net
|
||||
bind:
|
||||
dn: uid=ldap-reader-user,ou=people,ou=example,dc=example,dc=net
|
||||
secret: ${LDAP_SECRET}
|
||||
schedule:
|
||||
frequency: PT1H
|
||||
timeout: PT15M
|
||||
```
|
||||
|
||||
After this, you also have to add some configuration in your app-config that
|
||||
describes what you want to import for that target.
|
||||
Finally, updated your backend by adding the following line:
|
||||
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
backend.add(import('@backstage/plugin-catalog-backend/alpha'));
|
||||
/* highlight-add-start */
|
||||
backend.add(import('@backstage/plugin-catalog-backend-module-ldap'));
|
||||
/* highlight-add-end */
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -77,34 +57,32 @@ The following configuration is a small example of how a setup could look for
|
||||
importing groups and users from a corporate LDAP server.
|
||||
|
||||
```yaml
|
||||
ldap:
|
||||
catalog:
|
||||
providers:
|
||||
- target: ldaps://ds.example.net
|
||||
bind:
|
||||
dn: uid=ldap-reader-user,ou=people,ou=example,dc=example,dc=net
|
||||
secret: ${LDAP_SECRET}
|
||||
users:
|
||||
dn: ou=people,ou=example,dc=example,dc=net
|
||||
options:
|
||||
filter: (uid=*)
|
||||
map:
|
||||
description: l
|
||||
set:
|
||||
metadata.customField: 'hello'
|
||||
groups:
|
||||
dn: ou=access,ou=groups,ou=example,dc=example,dc=net
|
||||
options:
|
||||
filter: (&(objectClass=some-group-class)(!(groupType=email)))
|
||||
map:
|
||||
description: l
|
||||
set:
|
||||
metadata.customField: 'hello'
|
||||
ldapOrg:
|
||||
default:
|
||||
target: ldaps://ds.example.net
|
||||
bind:
|
||||
dn: uid=ldap-reader-user,ou=people,ou=example,dc=example,dc=net
|
||||
secret: ${LDAP_SECRET}
|
||||
users:
|
||||
dn: ou=people,ou=example,dc=example,dc=net
|
||||
options:
|
||||
filter: (uid=*)
|
||||
map:
|
||||
description: l
|
||||
set:
|
||||
metadata.customField: 'hello'
|
||||
groups:
|
||||
dn: ou=access,ou=groups,ou=example,dc=example,dc=net
|
||||
options:
|
||||
filter: (&(objectClass=some-group-class)(!(groupType=email)))
|
||||
map:
|
||||
description: l
|
||||
set:
|
||||
metadata.customField: 'hello'
|
||||
```
|
||||
|
||||
There may be many providers, each targeting a specific `target` which is
|
||||
supposed to match the `target` of a dedicated provider instance - i.e., you will
|
||||
add one entity provider class instance per target to ingest from.
|
||||
|
||||
These config blocks have a lot of options in them, so we will describe each
|
||||
"root" key within the block separately.
|
||||
|
||||
@@ -321,97 +299,34 @@ map:
|
||||
## Customize the Provider
|
||||
|
||||
In case you want to customize the ingested entities, the provider allows to pass
|
||||
transformers for users and groups. Here we will show an example of overriding
|
||||
the group transformer.
|
||||
transformers for users and groups.
|
||||
|
||||
1. Create a transformer:
|
||||
Transformers can be configured by extending `ldapOrgEntityProviderTransformExtensionPoint`. Here is an example:
|
||||
|
||||
```ts
|
||||
export async function myGroupTransformer(
|
||||
vendor: LdapVendor,
|
||||
config: GroupConfig,
|
||||
group: SearchEntry,
|
||||
): Promise<GroupEntity | undefined> {
|
||||
// Transformations may change namespace, change entity naming pattern, fill
|
||||
// profile with more or other details...
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
import { createBackendModule } from '@backstage/backend-plugin-api';
|
||||
import { ldapOrgEntityProviderTransformExtensionPoint } from '@backstage/plugin-catalog-backend-module-ldap';
|
||||
import { myUserTransformer, myGroupTransformer } from './transformers';
|
||||
|
||||
// Create the group entity on your own, or wrap the default transformer
|
||||
return await defaultGroupTransformer(vendor, config, group);
|
||||
}
|
||||
```
|
||||
|
||||
2. Configure the provider with the transformer:
|
||||
|
||||
```ts
|
||||
const ldapEntityProvider = LdapOrgEntityProvider.fromConfig(env.config, {
|
||||
id: 'our-ldap-master',
|
||||
target: 'ldaps://ds.example.net',
|
||||
logger: env.logger,
|
||||
groupTransformer: myGroupTransformer,
|
||||
});
|
||||
```
|
||||
|
||||
## Using a Processor instead of a Provider
|
||||
|
||||
An alternative to using the Provider for ingesting LDAP entries is to use a
|
||||
Processor. This is the old way that's based on registering locations with the
|
||||
proper type and target, triggering the processor to run.
|
||||
|
||||
The drawback of this method is that it will leave orphaned Group/User entities
|
||||
whenever they are deleted on your LDAP server, and you cannot control the
|
||||
frequency with which they are refreshed, separately from other processors.
|
||||
|
||||
### Processor Installation
|
||||
|
||||
The `LdapOrgReaderProcessor` is not registered by default, so you have to
|
||||
register it in the catalog plugin:
|
||||
|
||||
```typescript title="packages/backend/src/plugins/catalog.ts"
|
||||
builder.addProcessor(
|
||||
LdapOrgReaderProcessor.fromConfig(env.config, {
|
||||
logger: env.logger,
|
||||
backend.add(
|
||||
createBackendModule({
|
||||
pluginId: 'catalog',
|
||||
moduleId: 'ldap-extensions',
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
deps: {
|
||||
/* highlight-add-start */
|
||||
ldapTransformers: ldapOrgEntityProviderTransformExtensionPoint,
|
||||
/* highlight-add-end */
|
||||
},
|
||||
async init({ ldapTransformers }) {
|
||||
/* highlight-add-start */
|
||||
ldapTransformers.setUserTransformer(myUserTransformer);
|
||||
ldapTransformers.setGroupTransformer(myGroupTransformer);
|
||||
/* highlight-add-end */
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
```
|
||||
|
||||
### Driving LDAP Org Processor Ingestion with Locations
|
||||
|
||||
Locations point out the specific org(s) you want to import. The `type` of these
|
||||
locations must be `ldap-org`, and the `target` must point to the exact URL
|
||||
(starting with `ldap://` or `ldaps://`) of the targeted LDAP server. You can
|
||||
have several such location entries if you want, but typically you will have just
|
||||
one.
|
||||
|
||||
```yaml
|
||||
catalog:
|
||||
locations:
|
||||
- type: ldap-org
|
||||
target: ldaps://ds.example.net
|
||||
rules:
|
||||
- allow: [User, Group]
|
||||
```
|
||||
|
||||
### Example configurations
|
||||
|
||||
#### Google Secure LDAP Service
|
||||
|
||||
To sync Google Workspace/Cloud Identity organization data to users and groups in backstage,
|
||||
you must [configure Secure LDAP Service](https://support.google.com/a/answer/9048516) first.
|
||||
|
||||
Once Secure LDAP Service is configured, you can enable TLS options in LDAP configuration,
|
||||
as mentioned below. `keys` and `certs` specify the location of files that are generated
|
||||
while configuring Secure LDAP Service above.
|
||||
|
||||
```yaml
|
||||
ldap:
|
||||
providers:
|
||||
- target: ldaps://ldap.google.com:636
|
||||
tls:
|
||||
rejectUnauthorized: false
|
||||
keys: '/var/secrets/tls/gldap.key'
|
||||
certs: '/var/secrets/tls/gldap.crt'
|
||||
users:
|
||||
# users configuration comes here
|
||||
groups:
|
||||
# groups configuration comes here
|
||||
```
|
||||
|
||||
@@ -8,3 +8,7 @@ groups from your Active Directory or another LDAP compatible server.
|
||||
|
||||
See [Backstage documentation](https://backstage.io/docs/integrations/ldap/org) for details on how to install
|
||||
and configure the plugin.
|
||||
|
||||
## Legacy backend
|
||||
|
||||
You can find the legacy documentation at `docs/integrations/ldap/org--old.md`.
|
||||
|
||||
@@ -3,20 +3,26 @@
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
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 { Config } from '@backstage/config';
|
||||
import { EntityProvider } from '@backstage/plugin-catalog-node';
|
||||
import { EntityProviderConnection } from '@backstage/plugin-catalog-node';
|
||||
import { ExtensionPoint } from '@backstage/backend-plugin-api';
|
||||
import { GroupEntity } from '@backstage/catalog-model';
|
||||
import { GroupTransformer as GroupTransformer_2 } from '@backstage/plugin-catalog-backend-module-ldap';
|
||||
import { JsonValue } from '@backstage/types';
|
||||
import { LocationSpec } from '@backstage/plugin-catalog-common';
|
||||
import { LoggerService } from '@backstage/backend-plugin-api';
|
||||
import { PluginTaskScheduler } from '@backstage/backend-tasks';
|
||||
import { SearchEntry } from 'ldapjs';
|
||||
import { SearchOptions } from 'ldapjs';
|
||||
import { TaskRunner } from '@backstage/backend-tasks';
|
||||
import { TaskScheduleDefinition } from '@backstage/backend-tasks';
|
||||
import { UserEntity } from '@backstage/catalog-model';
|
||||
import { UserTransformer as UserTransformer_2 } from '@backstage/plugin-catalog-backend-module-ldap';
|
||||
|
||||
// @public
|
||||
export type BindConfig = {
|
||||
@@ -24,6 +30,10 @@ export type BindConfig = {
|
||||
secret: string;
|
||||
};
|
||||
|
||||
// @public
|
||||
const catalogModuleLdapOrgEntityProvider: () => BackendFeature;
|
||||
export default catalogModuleLdapOrgEntityProvider;
|
||||
|
||||
// @public
|
||||
export function defaultGroupTransformer(
|
||||
vendor: LdapVendor,
|
||||
@@ -109,14 +119,19 @@ export class LdapOrgEntityProvider implements EntityProvider {
|
||||
static fromConfig(
|
||||
configRoot: Config,
|
||||
options: LdapOrgEntityProviderOptions,
|
||||
): LdapOrgEntityProvider[];
|
||||
// (undocumented)
|
||||
static fromLegacyConfig(
|
||||
configRoot: Config,
|
||||
options: LdapOrgEntityProviderLegacyOptions,
|
||||
): LdapOrgEntityProvider;
|
||||
// (undocumented)
|
||||
getProviderName(): string;
|
||||
read(options?: { logger?: LoggerService }): Promise<void>;
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface LdapOrgEntityProviderOptions {
|
||||
// @public @deprecated
|
||||
export interface LdapOrgEntityProviderLegacyOptions {
|
||||
groupTransformer?: GroupTransformer;
|
||||
id: string;
|
||||
logger: LoggerService;
|
||||
@@ -125,6 +140,30 @@ export interface LdapOrgEntityProviderOptions {
|
||||
userTransformer?: UserTransformer;
|
||||
}
|
||||
|
||||
// @public
|
||||
export type LdapOrgEntityProviderOptions =
|
||||
| LdapOrgEntityProviderLegacyOptions
|
||||
| {
|
||||
logger: LoggerService;
|
||||
schedule?: 'manual' | TaskRunner;
|
||||
scheduler?: PluginTaskScheduler;
|
||||
userTransformer?: UserTransformer | Record<string, UserTransformer>;
|
||||
groupTransformer?: GroupTransformer | Record<string, GroupTransformer>;
|
||||
};
|
||||
|
||||
// @public
|
||||
export interface LdapOrgEntityProviderTransformsExtensionPoint {
|
||||
setGroupTransformer(
|
||||
transformer: GroupTransformer_2 | Record<string, GroupTransformer_2>,
|
||||
): void;
|
||||
setUserTransformer(
|
||||
transformer: UserTransformer_2 | Record<string, UserTransformer_2>,
|
||||
): void;
|
||||
}
|
||||
|
||||
// @public
|
||||
export const ldapOrgEntityProviderTransformsExtensionPoint: ExtensionPoint<LdapOrgEntityProviderTransformsExtensionPoint>;
|
||||
|
||||
// @public
|
||||
export class LdapOrgReaderProcessor implements CatalogProcessor {
|
||||
constructor(options: {
|
||||
@@ -154,11 +193,13 @@ export class LdapOrgReaderProcessor implements CatalogProcessor {
|
||||
|
||||
// @public
|
||||
export type LdapProviderConfig = {
|
||||
id: string;
|
||||
target: string;
|
||||
tls?: TLSConfig;
|
||||
bind?: BindConfig;
|
||||
users: UserConfig;
|
||||
groups: GroupConfig;
|
||||
schedule?: TaskScheduleDefinition;
|
||||
};
|
||||
|
||||
// @public
|
||||
@@ -176,8 +217,8 @@ export function mapStringAttr(
|
||||
setter: (value: string) => void,
|
||||
): void;
|
||||
|
||||
// @public
|
||||
export function readLdapConfig(config: Config): LdapProviderConfig[];
|
||||
// @public @deprecated
|
||||
export function readLdapLegacyConfig(config: Config): LdapProviderConfig[];
|
||||
|
||||
// @public
|
||||
export function readLdapOrg(
|
||||
@@ -194,6 +235,9 @@ export function readLdapOrg(
|
||||
groups: GroupEntity[];
|
||||
}>;
|
||||
|
||||
// @public
|
||||
export function readProviderConfigs(config: Config): LdapProviderConfig[];
|
||||
|
||||
// @public
|
||||
export type TLSConfig = {
|
||||
rejectUnauthorized?: boolean;
|
||||
|
||||
+229
-2
@@ -19,6 +19,8 @@ import { JsonValue } from '@backstage/types';
|
||||
export interface Config {
|
||||
/**
|
||||
* LdapOrgEntityProvider / LdapOrgReaderProcessor configuration
|
||||
*
|
||||
* @deprecated This exists for backwards compatibility only and will be removed in the future.
|
||||
*/
|
||||
ldap?: {
|
||||
/**
|
||||
@@ -240,12 +242,237 @@ export interface Config {
|
||||
|
||||
/**
|
||||
* Configuration options for the catalog plugin.
|
||||
*
|
||||
* TODO(freben): Deprecate this entire block
|
||||
*/
|
||||
catalog?: {
|
||||
/**
|
||||
* List of provider-specific options and attributes
|
||||
*/
|
||||
providers?: {
|
||||
/**
|
||||
* LdapOrg provider key
|
||||
*/
|
||||
ldapOrg: {
|
||||
/**
|
||||
* Id of the LdapOrg provider
|
||||
*/
|
||||
[id: string]: {
|
||||
/**
|
||||
* The prefix of the target that this matches on, e.g.
|
||||
* "ldaps://ds.example.net", with no trailing slash.
|
||||
*/
|
||||
target: string;
|
||||
|
||||
/**
|
||||
* The settings to use for the bind command. If none are specified,
|
||||
* the bind command is not issued.
|
||||
*/
|
||||
bind?: {
|
||||
/**
|
||||
* The DN of the user to auth as.
|
||||
*
|
||||
* E.g. "uid=ldap-robot,ou=robots,ou=example,dc=example,dc=net"
|
||||
*/
|
||||
dn: string;
|
||||
/**
|
||||
* The secret of the user to auth as (its password).
|
||||
*
|
||||
* @visibility secret
|
||||
*/
|
||||
secret: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* TLS settings
|
||||
*/
|
||||
tls?: {
|
||||
// Node TLS rejectUnauthorized
|
||||
rejectUnauthorized?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* The settings that govern the reading and interpretation of users.
|
||||
*/
|
||||
users: {
|
||||
/**
|
||||
* The DN under which users are stored.
|
||||
*
|
||||
* E.g. "ou=people,ou=example,dc=example,dc=net"
|
||||
*/
|
||||
dn: string;
|
||||
/**
|
||||
* The search options to use. The default is scope "one" and
|
||||
* attributes "*" and "+".
|
||||
*
|
||||
* It is common to want to specify a filter, to narrow down the set
|
||||
* of matching items.
|
||||
*/
|
||||
options: {
|
||||
scope?: 'base' | 'one' | 'sub';
|
||||
filter?: string;
|
||||
attributes?: string | string[];
|
||||
sizeLimit?: number;
|
||||
timeLimit?: number;
|
||||
derefAliases?: number;
|
||||
typesOnly?: boolean;
|
||||
paged?:
|
||||
| boolean
|
||||
| {
|
||||
pageSize?: number;
|
||||
pagePause?: boolean;
|
||||
};
|
||||
};
|
||||
/**
|
||||
* JSON paths (on a.b.c form) and hard coded values to set on those
|
||||
* paths.
|
||||
*
|
||||
* This can be useful for example if you want to hard code a
|
||||
* namespace or similar on the generated entities.
|
||||
*/
|
||||
set?: { [key: string]: JsonValue };
|
||||
/**
|
||||
* Mappings from well known entity fields, to LDAP attribute names
|
||||
*/
|
||||
map?: {
|
||||
/**
|
||||
* The name of the attribute that holds the relative
|
||||
* distinguished name of each entry. Defaults to "uid".
|
||||
*/
|
||||
rdn?: string;
|
||||
/**
|
||||
* The name of the attribute that shall be used for the value of
|
||||
* the metadata.name field of the entity. Defaults to "uid".
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* The name of the attribute that shall be used for the value of
|
||||
* the metadata.description field of the entity.
|
||||
*/
|
||||
description?: string;
|
||||
/**
|
||||
* The name of the attribute that shall be used for the value of
|
||||
* the spec.profile.displayName field of the entity. Defaults to
|
||||
* "cn".
|
||||
*/
|
||||
displayName?: string;
|
||||
/**
|
||||
* The name of the attribute that shall be used for the value of
|
||||
* the spec.profile.email field of the entity. Defaults to
|
||||
* "mail".
|
||||
*/
|
||||
email?: string;
|
||||
/**
|
||||
* The name of the attribute that shall be used for the value of
|
||||
* the spec.profile.picture field of the entity.
|
||||
*/
|
||||
picture?: string;
|
||||
/**
|
||||
* The name of the attribute that shall be used for the values of
|
||||
* the spec.memberOf field of the entity. Defaults to "memberOf".
|
||||
*/
|
||||
memberOf?: string;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* The settings that govern the reading and interpretation of groups.
|
||||
*/
|
||||
groups: {
|
||||
/**
|
||||
* The DN under which groups are stored.
|
||||
*
|
||||
* E.g. "ou=people,ou=example,dc=example,dc=net"
|
||||
*/
|
||||
dn: string;
|
||||
/**
|
||||
* The search options to use. The default is scope "one" and
|
||||
* attributes "*" and "+".
|
||||
*
|
||||
* It is common to want to specify a filter, to narrow down the set
|
||||
* of matching items.
|
||||
*/
|
||||
options: {
|
||||
scope?: 'base' | 'one' | 'sub';
|
||||
filter?: string;
|
||||
attributes?: string | string[];
|
||||
sizeLimit?: number;
|
||||
timeLimit?: number;
|
||||
derefAliases?: number;
|
||||
typesOnly?: boolean;
|
||||
paged?:
|
||||
| boolean
|
||||
| {
|
||||
pageSize?: number;
|
||||
pagePause?: boolean;
|
||||
};
|
||||
};
|
||||
/**
|
||||
* JSON paths (on a.b.c form) and hard coded values to set on those
|
||||
* paths.
|
||||
*
|
||||
* This can be useful for example if you want to hard code a
|
||||
* namespace or similar on the generated entities.
|
||||
*/
|
||||
set?: { [key: string]: JsonValue };
|
||||
/**
|
||||
* Mappings from well known entity fields, to LDAP attribute names
|
||||
*/
|
||||
map?: {
|
||||
/**
|
||||
* The name of the attribute that holds the relative
|
||||
* distinguished name of each entry. Defaults to "cn".
|
||||
*/
|
||||
rdn?: string;
|
||||
/**
|
||||
* The name of the attribute that shall be used for the value of
|
||||
* the metadata.name field of the entity. Defaults to "cn".
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* The name of the attribute that shall be used for the value of
|
||||
* the metadata.description field of the entity. Defaults to
|
||||
* "description".
|
||||
*/
|
||||
description?: string;
|
||||
/**
|
||||
* The name of the attribute that shall be used for the value of
|
||||
* the spec.type field of the entity. Defaults to "groupType".
|
||||
*/
|
||||
type?: string;
|
||||
/**
|
||||
* The name of the attribute that shall be used for the value of
|
||||
* the spec.profile.displayName field of the entity. Defaults to
|
||||
* "cn".
|
||||
*/
|
||||
displayName?: string;
|
||||
/**
|
||||
* The name of the attribute that shall be used for the value of
|
||||
* the spec.profile.email field of the entity.
|
||||
*/
|
||||
email?: string;
|
||||
/**
|
||||
* The name of the attribute that shall be used for the value of
|
||||
* the spec.profile.picture field of the entity.
|
||||
*/
|
||||
picture?: string;
|
||||
/**
|
||||
* The name of the attribute that shall be used for the values of
|
||||
* the spec.parent field of the entity. Defaults to "memberOf".
|
||||
*/
|
||||
memberOf?: string;
|
||||
/**
|
||||
* The name of the attribute that shall be used for the values of
|
||||
* the spec.children field of the entity. Defaults to "member".
|
||||
*/
|
||||
members?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* List of processor-specific options and attributes
|
||||
*
|
||||
* @deprecated This exists for backwards compatibility only and will be removed in the future.
|
||||
*/
|
||||
processors?: {
|
||||
/**
|
||||
|
||||
@@ -22,3 +22,8 @@
|
||||
|
||||
export * from './processors';
|
||||
export * from './ldap';
|
||||
export {
|
||||
catalogModuleLdapOrgEntityProvider as default,
|
||||
ldapOrgEntityProviderTransformsExtensionPoint,
|
||||
type LdapOrgEntityProviderTransformsExtensionPoint,
|
||||
} from './module';
|
||||
|
||||
@@ -15,26 +15,31 @@
|
||||
*/
|
||||
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { readLdapConfig } from './config';
|
||||
import { readProviderConfigs } from './config';
|
||||
|
||||
describe('readLdapConfig', () => {
|
||||
it('applies all of the defaults', () => {
|
||||
const config = {
|
||||
providers: [
|
||||
{
|
||||
target: 'target',
|
||||
users: {
|
||||
dn: 'udn',
|
||||
},
|
||||
groups: {
|
||||
dn: 'gdn',
|
||||
catalog: {
|
||||
providers: {
|
||||
ldapOrg: {
|
||||
default: {
|
||||
target: 'target',
|
||||
users: {
|
||||
dn: 'udn',
|
||||
},
|
||||
groups: {
|
||||
dn: 'gdn',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const actual = readLdapConfig(new ConfigReader(config));
|
||||
const actual = readProviderConfigs(new ConfigReader(config));
|
||||
const expected = [
|
||||
{
|
||||
id: 'default',
|
||||
target: 'target',
|
||||
bind: undefined,
|
||||
users: {
|
||||
@@ -76,72 +81,77 @@ describe('readLdapConfig', () => {
|
||||
|
||||
it('reads all the values', () => {
|
||||
const config = {
|
||||
providers: [
|
||||
{
|
||||
target: 'target',
|
||||
bind: { dn: 'bdn', secret: 's' },
|
||||
tls: {
|
||||
rejectUnauthorized: false,
|
||||
keys: '/tmp/keys.pem',
|
||||
certs: '/tmp/certs.pem',
|
||||
},
|
||||
users: {
|
||||
dn: 'udn',
|
||||
options: {
|
||||
scope: 'base',
|
||||
attributes: ['*'],
|
||||
filter: 'f',
|
||||
paged: true,
|
||||
timeLimit: 42,
|
||||
sizeLimit: 100,
|
||||
derefAliases: 0,
|
||||
typesOnly: false,
|
||||
},
|
||||
set: { p: 'v' },
|
||||
map: {
|
||||
rdn: 'u',
|
||||
name: 'v',
|
||||
description: 'd',
|
||||
displayName: 'c',
|
||||
email: 'm',
|
||||
picture: 'p',
|
||||
memberOf: 'm',
|
||||
},
|
||||
},
|
||||
groups: {
|
||||
dn: 'gdn',
|
||||
options: {
|
||||
scope: 'base',
|
||||
attributes: ['*'],
|
||||
filter: 'f',
|
||||
paged: {
|
||||
pageSize: 7,
|
||||
pagePause: true,
|
||||
catalog: {
|
||||
providers: {
|
||||
ldapOrg: {
|
||||
default: {
|
||||
target: 'target',
|
||||
bind: { dn: 'bdn', secret: 's' },
|
||||
tls: {
|
||||
rejectUnauthorized: false,
|
||||
keys: '/tmp/keys.pem',
|
||||
certs: '/tmp/certs.pem',
|
||||
},
|
||||
users: {
|
||||
dn: 'udn',
|
||||
options: {
|
||||
scope: 'base',
|
||||
attributes: ['*'],
|
||||
filter: 'f',
|
||||
paged: true,
|
||||
timeLimit: 42,
|
||||
sizeLimit: 100,
|
||||
derefAliases: 0,
|
||||
typesOnly: false,
|
||||
},
|
||||
set: { p: 'v' },
|
||||
map: {
|
||||
rdn: 'u',
|
||||
name: 'v',
|
||||
description: 'd',
|
||||
displayName: 'c',
|
||||
email: 'm',
|
||||
picture: 'p',
|
||||
memberOf: 'm',
|
||||
},
|
||||
},
|
||||
groups: {
|
||||
dn: 'gdn',
|
||||
options: {
|
||||
scope: 'base',
|
||||
attributes: ['*'],
|
||||
filter: 'f',
|
||||
paged: {
|
||||
pageSize: 7,
|
||||
pagePause: true,
|
||||
},
|
||||
timeLimit: 42,
|
||||
sizeLimit: 100,
|
||||
derefAliases: 1,
|
||||
typesOnly: true,
|
||||
},
|
||||
set: { p: 'v' },
|
||||
map: {
|
||||
rdn: 'u',
|
||||
name: 'v',
|
||||
description: 'd',
|
||||
type: 't',
|
||||
displayName: 'c',
|
||||
email: 'm',
|
||||
picture: 'p',
|
||||
memberOf: 'm',
|
||||
members: 'n',
|
||||
},
|
||||
},
|
||||
timeLimit: 42,
|
||||
sizeLimit: 100,
|
||||
derefAliases: 1,
|
||||
typesOnly: true,
|
||||
},
|
||||
set: { p: 'v' },
|
||||
map: {
|
||||
rdn: 'u',
|
||||
name: 'v',
|
||||
description: 'd',
|
||||
type: 't',
|
||||
displayName: 'c',
|
||||
email: 'm',
|
||||
picture: 'p',
|
||||
memberOf: 'm',
|
||||
members: 'n',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const actual = readLdapConfig(new ConfigReader(config));
|
||||
const actual = readProviderConfigs(new ConfigReader(config));
|
||||
const expected = [
|
||||
{
|
||||
id: 'default',
|
||||
target: 'target',
|
||||
bind: { dn: 'bdn', secret: 's' },
|
||||
tls: {
|
||||
@@ -207,30 +217,34 @@ describe('readLdapConfig', () => {
|
||||
|
||||
it('supports multiline ldap query filter', () => {
|
||||
const config = {
|
||||
providers: [
|
||||
{
|
||||
target: 'target',
|
||||
users: {
|
||||
dn: 'udn',
|
||||
options: {
|
||||
filter: `
|
||||
(|
|
||||
(cn=foo bar)
|
||||
(cn=bar)
|
||||
)
|
||||
`,
|
||||
},
|
||||
},
|
||||
groups: {
|
||||
dn: 'gdn',
|
||||
options: {
|
||||
filter: 'f',
|
||||
catalog: {
|
||||
providers: {
|
||||
ldapOrg: {
|
||||
default: {
|
||||
target: 'target',
|
||||
users: {
|
||||
dn: 'udn',
|
||||
options: {
|
||||
filter: `
|
||||
(|
|
||||
(cn=foo bar)
|
||||
(cn=bar)
|
||||
)
|
||||
`,
|
||||
},
|
||||
},
|
||||
groups: {
|
||||
dn: 'gdn',
|
||||
options: {
|
||||
filter: 'f',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const actual = readLdapConfig(new ConfigReader(config));
|
||||
const actual = readProviderConfigs(new ConfigReader(config));
|
||||
|
||||
const expected = '(|(cn=foo bar)(cn=bar))';
|
||||
expect(actual[0].users.options.filter).toEqual(expected);
|
||||
@@ -238,64 +252,72 @@ describe('readLdapConfig', () => {
|
||||
|
||||
it('supports a dot nested set structure', () => {
|
||||
const config = {
|
||||
providers: [
|
||||
{
|
||||
target: 'target',
|
||||
users: {
|
||||
dn: 'udn',
|
||||
options: {
|
||||
filter: 'f',
|
||||
},
|
||||
set: {
|
||||
'metadata.annotations': {
|
||||
a: 'b',
|
||||
catalog: {
|
||||
providers: {
|
||||
ldapOrg: {
|
||||
default: {
|
||||
target: 'target',
|
||||
users: {
|
||||
dn: 'udn',
|
||||
options: {
|
||||
filter: 'f',
|
||||
},
|
||||
set: {
|
||||
'metadata.annotations': {
|
||||
a: 'b',
|
||||
},
|
||||
},
|
||||
},
|
||||
groups: {
|
||||
dn: 'gdn',
|
||||
options: {
|
||||
filter: 'f',
|
||||
},
|
||||
set: {
|
||||
x: { a: 'b' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
groups: {
|
||||
dn: 'gdn',
|
||||
options: {
|
||||
filter: 'f',
|
||||
},
|
||||
set: {
|
||||
x: { a: 'b' },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const actual = readLdapConfig(new ConfigReader(config));
|
||||
const actual = readProviderConfigs(new ConfigReader(config));
|
||||
|
||||
expect(actual[0].users.set).toEqual({ 'metadata.annotations': { a: 'b' } });
|
||||
});
|
||||
|
||||
it('throws on attempts to modify the set structure', () => {
|
||||
const config = {
|
||||
providers: [
|
||||
{
|
||||
target: 'target',
|
||||
users: {
|
||||
dn: 'udn',
|
||||
options: {
|
||||
filter: 'f',
|
||||
},
|
||||
set: {
|
||||
x: { a: 'b' },
|
||||
},
|
||||
},
|
||||
groups: {
|
||||
dn: 'gdn',
|
||||
options: {
|
||||
filter: 'f',
|
||||
},
|
||||
set: {
|
||||
x: { a: 'b' },
|
||||
catalog: {
|
||||
providers: {
|
||||
ldapOrg: {
|
||||
default: {
|
||||
target: 'target',
|
||||
users: {
|
||||
dn: 'udn',
|
||||
options: {
|
||||
filter: 'f',
|
||||
},
|
||||
set: {
|
||||
x: { a: 'b' },
|
||||
},
|
||||
},
|
||||
groups: {
|
||||
dn: 'gdn',
|
||||
options: {
|
||||
filter: 'f',
|
||||
},
|
||||
set: {
|
||||
x: { a: 'b' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const actual = readLdapConfig(new ConfigReader(config));
|
||||
const actual = readProviderConfigs(new ConfigReader(config));
|
||||
|
||||
expect(() => {
|
||||
(actual[0].users.set as any).y = 2;
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
readTaskScheduleDefinitionFromConfig,
|
||||
TaskScheduleDefinition,
|
||||
} from '@backstage/backend-tasks';
|
||||
import { Config } from '@backstage/config';
|
||||
import { JsonValue } from '@backstage/types';
|
||||
import { SearchOptions } from 'ldapjs';
|
||||
@@ -27,6 +31,8 @@ import { RecursivePartial } from './util';
|
||||
* @public
|
||||
*/
|
||||
export type LdapProviderConfig = {
|
||||
// The id of the
|
||||
id: string;
|
||||
// The prefix of the target that this matches on, e.g.
|
||||
// "ldaps://ds.example.net", with no trailing slash.
|
||||
target: string;
|
||||
@@ -39,6 +45,8 @@ export type LdapProviderConfig = {
|
||||
users: UserConfig;
|
||||
// The settings that govern the reading and interpretation of groups
|
||||
groups: GroupConfig;
|
||||
// Schedule configuration for refresh tasks.
|
||||
schedule?: TaskScheduleDefinition;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -184,159 +192,160 @@ const defaultConfig = {
|
||||
},
|
||||
};
|
||||
|
||||
function freeze<T>(data: T): T {
|
||||
return JSON.parse(JSON.stringify(data), (_key, value) => {
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
Object.freeze(value);
|
||||
}
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
||||
function readTlsConfig(
|
||||
c: Config | undefined,
|
||||
): LdapProviderConfig['tls'] | undefined {
|
||||
if (!c) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
rejectUnauthorized: c.getOptionalBoolean('rejectUnauthorized'),
|
||||
keys: c.getOptionalString('keys'),
|
||||
certs: c.getOptionalString('certs'),
|
||||
};
|
||||
}
|
||||
|
||||
function readBindConfig(
|
||||
c: Config | undefined,
|
||||
): LdapProviderConfig['bind'] | undefined {
|
||||
if (!c) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
dn: c.getString('dn'),
|
||||
secret: c.getString('secret'),
|
||||
};
|
||||
}
|
||||
|
||||
function readOptionsConfig(c: Config | undefined): SearchOptions {
|
||||
if (!c) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const paged = readOptionsPagedConfig(c);
|
||||
|
||||
return {
|
||||
scope: c.getOptionalString('scope') as SearchOptions['scope'],
|
||||
filter: formatFilter(c.getOptionalString('filter')),
|
||||
attributes: c.getOptionalStringArray('attributes'),
|
||||
sizeLimit: c.getOptionalNumber('sizeLimit'),
|
||||
timeLimit: c.getOptionalNumber('timeLimit'),
|
||||
derefAliases: c.getOptionalNumber('derefAliases'),
|
||||
typesOnly: c.getOptionalBoolean('typesOnly'),
|
||||
...(paged !== undefined ? { paged } : undefined),
|
||||
};
|
||||
}
|
||||
|
||||
function readOptionsPagedConfig(c: Config): SearchOptions['paged'] {
|
||||
const pagedConfig = c.getOptional('paged');
|
||||
if (pagedConfig === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (pagedConfig === true || pagedConfig === false) {
|
||||
return pagedConfig;
|
||||
}
|
||||
|
||||
const pageSize = c.getOptionalNumber('paged.pageSize');
|
||||
const pagePause = c.getOptionalBoolean('paged.pagePause');
|
||||
return {
|
||||
...(pageSize !== undefined ? { pageSize } : undefined),
|
||||
...(pagePause !== undefined ? { pagePause } : undefined),
|
||||
};
|
||||
}
|
||||
|
||||
function readSetConfig(
|
||||
c: Config | undefined,
|
||||
): { [path: string]: JsonValue } | undefined {
|
||||
if (!c) {
|
||||
return undefined;
|
||||
}
|
||||
return c.get();
|
||||
}
|
||||
|
||||
function readUserMapConfig(
|
||||
c: Config | undefined,
|
||||
): Partial<LdapProviderConfig['users']['map']> {
|
||||
if (!c) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
rdn: c.getOptionalString('rdn'),
|
||||
name: c.getOptionalString('name'),
|
||||
description: c.getOptionalString('description'),
|
||||
displayName: c.getOptionalString('displayName'),
|
||||
email: c.getOptionalString('email'),
|
||||
picture: c.getOptionalString('picture'),
|
||||
memberOf: c.getOptionalString('memberOf'),
|
||||
};
|
||||
}
|
||||
|
||||
function readGroupMapConfig(
|
||||
c: Config | undefined,
|
||||
): Partial<LdapProviderConfig['groups']['map']> {
|
||||
if (!c) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
rdn: c.getOptionalString('rdn'),
|
||||
name: c.getOptionalString('name'),
|
||||
description: c.getOptionalString('description'),
|
||||
type: c.getOptionalString('type'),
|
||||
displayName: c.getOptionalString('displayName'),
|
||||
email: c.getOptionalString('email'),
|
||||
picture: c.getOptionalString('picture'),
|
||||
memberOf: c.getOptionalString('memberOf'),
|
||||
members: c.getOptionalString('members'),
|
||||
};
|
||||
}
|
||||
|
||||
function readUserConfig(
|
||||
c: Config,
|
||||
): RecursivePartial<LdapProviderConfig['users']> {
|
||||
return {
|
||||
dn: c.getString('dn'),
|
||||
options: readOptionsConfig(c.getOptionalConfig('options')),
|
||||
set: readSetConfig(c.getOptionalConfig('set')),
|
||||
map: readUserMapConfig(c.getOptionalConfig('map')),
|
||||
};
|
||||
}
|
||||
|
||||
function readGroupConfig(
|
||||
c: Config,
|
||||
): RecursivePartial<LdapProviderConfig['groups']> {
|
||||
return {
|
||||
dn: c.getString('dn'),
|
||||
options: readOptionsConfig(c.getOptionalConfig('options')),
|
||||
set: readSetConfig(c.getOptionalConfig('set')),
|
||||
map: readGroupMapConfig(c.getOptionalConfig('map')),
|
||||
};
|
||||
}
|
||||
|
||||
function formatFilter(filter?: string): string | undefined {
|
||||
// Remove extra whitespace between blocks to support multiline filters from the configuration
|
||||
return filter?.replace(/\s*(\(|\))/g, '$1')?.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses configuration.
|
||||
*
|
||||
* @param config - The root of the LDAP config hierarchy
|
||||
*
|
||||
* @public
|
||||
* @deprecated This exists for backwards compatibility only and will be removed in the future.
|
||||
*/
|
||||
export function readLdapConfig(config: Config): LdapProviderConfig[] {
|
||||
function freeze<T>(data: T): T {
|
||||
return JSON.parse(JSON.stringify(data), (_key, value) => {
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
Object.freeze(value);
|
||||
}
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
||||
function readTlsConfig(
|
||||
c: Config | undefined,
|
||||
): LdapProviderConfig['tls'] | undefined {
|
||||
if (!c) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
rejectUnauthorized: c.getOptionalBoolean('rejectUnauthorized'),
|
||||
keys: c.getOptionalString('keys'),
|
||||
certs: c.getOptionalString('certs'),
|
||||
};
|
||||
}
|
||||
|
||||
function readBindConfig(
|
||||
c: Config | undefined,
|
||||
): LdapProviderConfig['bind'] | undefined {
|
||||
if (!c) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
dn: c.getString('dn'),
|
||||
secret: c.getString('secret'),
|
||||
};
|
||||
}
|
||||
|
||||
function readOptionsConfig(c: Config | undefined): SearchOptions {
|
||||
if (!c) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const paged = readOptionsPagedConfig(c);
|
||||
|
||||
return {
|
||||
scope: c.getOptionalString('scope') as SearchOptions['scope'],
|
||||
filter: formatFilter(c.getOptionalString('filter')),
|
||||
attributes: c.getOptionalStringArray('attributes'),
|
||||
sizeLimit: c.getOptionalNumber('sizeLimit'),
|
||||
timeLimit: c.getOptionalNumber('timeLimit'),
|
||||
derefAliases: c.getOptionalNumber('derefAliases'),
|
||||
typesOnly: c.getOptionalBoolean('typesOnly'),
|
||||
...(paged !== undefined ? { paged } : undefined),
|
||||
};
|
||||
}
|
||||
|
||||
function readOptionsPagedConfig(c: Config): SearchOptions['paged'] {
|
||||
const pagedConfig = c.getOptional('paged');
|
||||
if (pagedConfig === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (pagedConfig === true || pagedConfig === false) {
|
||||
return pagedConfig;
|
||||
}
|
||||
|
||||
const pageSize = c.getOptionalNumber('paged.pageSize');
|
||||
const pagePause = c.getOptionalBoolean('paged.pagePause');
|
||||
return {
|
||||
...(pageSize !== undefined ? { pageSize } : undefined),
|
||||
...(pagePause !== undefined ? { pagePause } : undefined),
|
||||
};
|
||||
}
|
||||
|
||||
function readSetConfig(
|
||||
c: Config | undefined,
|
||||
): { [path: string]: JsonValue } | undefined {
|
||||
if (!c) {
|
||||
return undefined;
|
||||
}
|
||||
return c.get();
|
||||
}
|
||||
|
||||
function readUserMapConfig(
|
||||
c: Config | undefined,
|
||||
): Partial<LdapProviderConfig['users']['map']> {
|
||||
if (!c) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
rdn: c.getOptionalString('rdn'),
|
||||
name: c.getOptionalString('name'),
|
||||
description: c.getOptionalString('description'),
|
||||
displayName: c.getOptionalString('displayName'),
|
||||
email: c.getOptionalString('email'),
|
||||
picture: c.getOptionalString('picture'),
|
||||
memberOf: c.getOptionalString('memberOf'),
|
||||
};
|
||||
}
|
||||
|
||||
function readGroupMapConfig(
|
||||
c: Config | undefined,
|
||||
): Partial<LdapProviderConfig['groups']['map']> {
|
||||
if (!c) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
rdn: c.getOptionalString('rdn'),
|
||||
name: c.getOptionalString('name'),
|
||||
description: c.getOptionalString('description'),
|
||||
type: c.getOptionalString('type'),
|
||||
displayName: c.getOptionalString('displayName'),
|
||||
email: c.getOptionalString('email'),
|
||||
picture: c.getOptionalString('picture'),
|
||||
memberOf: c.getOptionalString('memberOf'),
|
||||
members: c.getOptionalString('members'),
|
||||
};
|
||||
}
|
||||
|
||||
function readUserConfig(
|
||||
c: Config,
|
||||
): RecursivePartial<LdapProviderConfig['users']> {
|
||||
return {
|
||||
dn: c.getString('dn'),
|
||||
options: readOptionsConfig(c.getOptionalConfig('options')),
|
||||
set: readSetConfig(c.getOptionalConfig('set')),
|
||||
map: readUserMapConfig(c.getOptionalConfig('map')),
|
||||
};
|
||||
}
|
||||
|
||||
function readGroupConfig(
|
||||
c: Config,
|
||||
): RecursivePartial<LdapProviderConfig['groups']> {
|
||||
return {
|
||||
dn: c.getString('dn'),
|
||||
options: readOptionsConfig(c.getOptionalConfig('options')),
|
||||
set: readSetConfig(c.getOptionalConfig('set')),
|
||||
map: readGroupMapConfig(c.getOptionalConfig('map')),
|
||||
};
|
||||
}
|
||||
|
||||
function formatFilter(filter?: string): string | undefined {
|
||||
// Remove extra whitespace between blocks to support multiline filters from the configuration
|
||||
return filter?.replace(/\s*(\(|\))/g, '$1')?.trim();
|
||||
}
|
||||
|
||||
export function readLdapLegacyConfig(config: Config): LdapProviderConfig[] {
|
||||
const providerConfigs = config.getOptionalConfigArray('providers') ?? [];
|
||||
return providerConfigs.map(c => {
|
||||
const newConfig = {
|
||||
@@ -353,3 +362,40 @@ export function readLdapConfig(config: Config): LdapProviderConfig[] {
|
||||
return freeze(merged) as LdapProviderConfig;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses all configured providers.
|
||||
*
|
||||
* @param config - The root of the LDAP config hierarchy
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function readProviderConfigs(config: Config): LdapProviderConfig[] {
|
||||
const providersConfig = config.getOptionalConfig('catalog.providers.ldapOrg');
|
||||
if (!providersConfig) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return providersConfig.keys().map(id => {
|
||||
const c = providersConfig.getConfig(id);
|
||||
|
||||
const schedule = c.has('schedule')
|
||||
? readTaskScheduleDefinitionFromConfig(c.getConfig('schedule'))
|
||||
: undefined;
|
||||
|
||||
const newConfig = {
|
||||
id,
|
||||
target: trimEnd(c.getString('target'), '/'),
|
||||
tls: readTlsConfig(c.getOptionalConfig('tls')),
|
||||
bind: readBindConfig(c.getOptionalConfig('bind')),
|
||||
users: readUserConfig(c.getConfig('users')),
|
||||
groups: readGroupConfig(c.getConfig('groups')),
|
||||
schedule,
|
||||
};
|
||||
const merged = mergeWith({}, defaultConfig, newConfig, (_into, from) => {
|
||||
// Replace arrays instead of merging, otherwise default behavior
|
||||
return Array.isArray(from) ? from : undefined;
|
||||
});
|
||||
return freeze(merged) as LdapProviderConfig;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
export { LdapClient } from './client';
|
||||
export { mapStringAttr } from './util';
|
||||
export { readLdapConfig } from './config';
|
||||
export { readProviderConfigs, readLdapLegacyConfig } from './config';
|
||||
export type {
|
||||
LdapProviderConfig,
|
||||
GroupConfig,
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Copyright 2022 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 {
|
||||
coreServices,
|
||||
createBackendModule,
|
||||
createExtensionPoint,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha';
|
||||
import {
|
||||
GroupTransformer,
|
||||
UserTransformer,
|
||||
} from '@backstage/plugin-catalog-backend-module-ldap';
|
||||
import { LdapOrgEntityProvider } from './processors';
|
||||
|
||||
/**
|
||||
* Interface for {@link LdapOrgEntityProviderTransformsExtensionPoint}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface LdapOrgEntityProviderTransformsExtensionPoint {
|
||||
/**
|
||||
* Set the function that transforms a user entry in LDAP to an entity.
|
||||
* Optionally, you can pass separate transformers per provider ID.
|
||||
*/
|
||||
setUserTransformer(
|
||||
transformer: UserTransformer | Record<string, UserTransformer>,
|
||||
): void;
|
||||
|
||||
/**
|
||||
* Set the function that transforms a group entry in LDAP to an entity.
|
||||
* Optionally, you can pass separate transformers per provider ID.
|
||||
*/
|
||||
setGroupTransformer(
|
||||
transformer: GroupTransformer | Record<string, GroupTransformer>,
|
||||
): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension point used to customize the transforms used by the module.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const ldapOrgEntityProviderTransformsExtensionPoint =
|
||||
createExtensionPoint<LdapOrgEntityProviderTransformsExtensionPoint>({
|
||||
id: 'catalog.ldapOrgEntityProvider.transforms',
|
||||
});
|
||||
|
||||
/**
|
||||
* Registers the LdapOrgEntityProvider with the catalog processing extension point.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const catalogModuleLdapOrgEntityProvider = createBackendModule({
|
||||
pluginId: 'catalog',
|
||||
moduleId: 'ldapOrgEntityProvider',
|
||||
register(env) {
|
||||
let userTransformer:
|
||||
| UserTransformer
|
||||
| Record<string, UserTransformer>
|
||||
| undefined;
|
||||
let groupTransformer:
|
||||
| GroupTransformer
|
||||
| Record<string, GroupTransformer>
|
||||
| undefined;
|
||||
|
||||
env.registerExtensionPoint(ldapOrgEntityProviderTransformsExtensionPoint, {
|
||||
setUserTransformer(transformer) {
|
||||
if (userTransformer) {
|
||||
throw new Error('User transformer may only be set once');
|
||||
}
|
||||
userTransformer = transformer;
|
||||
},
|
||||
setGroupTransformer(transformer) {
|
||||
if (groupTransformer) {
|
||||
throw new Error('Group transformer may only be set once');
|
||||
}
|
||||
groupTransformer = transformer;
|
||||
},
|
||||
});
|
||||
|
||||
env.registerInit({
|
||||
deps: {
|
||||
catalog: catalogProcessingExtensionPoint,
|
||||
config: coreServices.rootConfig,
|
||||
logger: coreServices.logger,
|
||||
scheduler: coreServices.scheduler,
|
||||
},
|
||||
async init({ catalog, config, logger, scheduler }) {
|
||||
catalog.addEntityProvider(
|
||||
LdapOrgEntityProvider.fromConfig(config, {
|
||||
logger,
|
||||
scheduler,
|
||||
userTransformer: userTransformer,
|
||||
groupTransformer: groupTransformer,
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { TaskRunner } from '@backstage/backend-tasks';
|
||||
import { PluginTaskScheduler, TaskRunner } from '@backstage/backend-tasks';
|
||||
import {
|
||||
ANNOTATION_LOCATION,
|
||||
ANNOTATION_ORIGIN_LOCATION,
|
||||
@@ -32,18 +32,65 @@ import {
|
||||
LdapClient,
|
||||
LdapProviderConfig,
|
||||
LDAP_DN_ANNOTATION,
|
||||
readLdapConfig,
|
||||
readLdapOrg,
|
||||
UserTransformer,
|
||||
} from '../ldap';
|
||||
import { LoggerService } from '@backstage/backend-plugin-api';
|
||||
import { readLdapLegacyConfig, readProviderConfigs } from '../ldap';
|
||||
|
||||
/**
|
||||
* Options for {@link LdapOrgEntityProvider}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface LdapOrgEntityProviderOptions {
|
||||
export type LdapOrgEntityProviderOptions =
|
||||
| LdapOrgEntityProviderLegacyOptions
|
||||
| {
|
||||
/**
|
||||
* The logger to use.
|
||||
*/
|
||||
logger: LoggerService;
|
||||
|
||||
/**
|
||||
* The refresh schedule to use.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* If you pass in 'manual', you are responsible for calling the `read` method
|
||||
* manually at some interval.
|
||||
*
|
||||
* But more commonly you will pass in the result of
|
||||
* {@link @backstage/backend-tasks#PluginTaskScheduler.createScheduledTaskRunner}
|
||||
* to enable automatic scheduling of tasks.
|
||||
*/
|
||||
schedule?: 'manual' | TaskRunner;
|
||||
|
||||
/**
|
||||
* Scheduler used to schedule refreshes based on
|
||||
* the schedule config.
|
||||
*/
|
||||
scheduler?: PluginTaskScheduler;
|
||||
|
||||
/**
|
||||
* The function that transforms a user entry in msgraph to an entity.
|
||||
* Optionally, you can pass separate transformers per provider ID.
|
||||
*/
|
||||
userTransformer?: UserTransformer | Record<string, UserTransformer>;
|
||||
|
||||
/**
|
||||
* The function that transforms a group entry in msgraph to an entity.
|
||||
* Optionally, you can pass separate transformers per provider ID.
|
||||
*/
|
||||
groupTransformer?: GroupTransformer | Record<string, GroupTransformer>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Options for {@link LdapOrgEntityProvider}.
|
||||
*
|
||||
* @public
|
||||
* @deprecated This interface exists for backwards compatibility only and will be removed in the future.
|
||||
*/
|
||||
export interface LdapOrgEntityProviderLegacyOptions {
|
||||
/**
|
||||
* A unique, stable identifier for this provider.
|
||||
*
|
||||
@@ -109,12 +156,68 @@ export class LdapOrgEntityProvider implements EntityProvider {
|
||||
static fromConfig(
|
||||
configRoot: Config,
|
||||
options: LdapOrgEntityProviderOptions,
|
||||
): LdapOrgEntityProvider[] {
|
||||
if ('id' in options) {
|
||||
return [LdapOrgEntityProvider.fromLegacyConfig(configRoot, options)];
|
||||
}
|
||||
|
||||
if (!options.schedule && !options.scheduler) {
|
||||
throw new Error('Either schedule or scheduler must be provided.');
|
||||
}
|
||||
|
||||
function getTransformer<T extends Function>(
|
||||
id: string,
|
||||
transformers?: T | Record<string, T>,
|
||||
): T | undefined {
|
||||
if (['undefined', 'function'].includes(typeof transformers)) {
|
||||
return transformers as T;
|
||||
}
|
||||
|
||||
return (transformers as Record<string, T>)[id];
|
||||
}
|
||||
|
||||
return readProviderConfigs(configRoot).map(providerConfig => {
|
||||
if (!options.schedule && !providerConfig.schedule) {
|
||||
throw new Error(
|
||||
`No schedule provided neither via code nor config for LdapOrgEntityProvider:${providerConfig.id}.`,
|
||||
);
|
||||
}
|
||||
|
||||
const taskRunner =
|
||||
options.schedule ??
|
||||
options.scheduler!.createScheduledTaskRunner(providerConfig.schedule!);
|
||||
|
||||
const provider = new LdapOrgEntityProvider({
|
||||
id: providerConfig.id,
|
||||
provider: providerConfig,
|
||||
logger: options.logger,
|
||||
userTransformer: getTransformer(
|
||||
providerConfig.id,
|
||||
options.userTransformer,
|
||||
),
|
||||
groupTransformer: getTransformer(
|
||||
providerConfig.id,
|
||||
options.groupTransformer,
|
||||
),
|
||||
});
|
||||
|
||||
if (taskRunner !== 'manual') {
|
||||
provider.schedule(taskRunner);
|
||||
}
|
||||
|
||||
return provider;
|
||||
});
|
||||
}
|
||||
|
||||
static fromLegacyConfig(
|
||||
configRoot: Config,
|
||||
options: LdapOrgEntityProviderLegacyOptions,
|
||||
): LdapOrgEntityProvider {
|
||||
// TODO(freben): Deprecate the old catalog.processors.ldapOrg config
|
||||
const config =
|
||||
configRoot.getOptionalConfig('ldap') ||
|
||||
configRoot.getOptionalConfig('catalog.processors.ldapOrg');
|
||||
const providers = config ? readLdapConfig(config) : [];
|
||||
const providers = config ? readLdapLegacyConfig(config) : [];
|
||||
const provider = providers.find(p => options.target === p.target);
|
||||
if (!provider) {
|
||||
throw new TypeError(
|
||||
@@ -134,7 +237,9 @@ export class LdapOrgEntityProvider implements EntityProvider {
|
||||
logger,
|
||||
});
|
||||
|
||||
result.schedule(options.schedule);
|
||||
if (options.schedule !== 'manual') {
|
||||
result.schedule(options.schedule);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -206,14 +311,10 @@ export class LdapOrgEntityProvider implements EntityProvider {
|
||||
markCommitComplete();
|
||||
}
|
||||
|
||||
private schedule(schedule: LdapOrgEntityProviderOptions['schedule']) {
|
||||
if (schedule === 'manual') {
|
||||
return;
|
||||
}
|
||||
|
||||
private schedule(taskRunner: TaskRunner) {
|
||||
this.scheduleFn = async () => {
|
||||
const id = `${this.getProviderName()}:refresh`;
|
||||
await schedule.run({
|
||||
await taskRunner.run({
|
||||
id,
|
||||
fn: async () => {
|
||||
const logger = this.options.logger.child({
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
GroupTransformer,
|
||||
LdapClient,
|
||||
LdapProviderConfig,
|
||||
readLdapConfig,
|
||||
readLdapLegacyConfig,
|
||||
readLdapOrg,
|
||||
UserTransformer,
|
||||
} from '../ldap';
|
||||
@@ -56,7 +56,7 @@ export class LdapOrgReaderProcessor implements CatalogProcessor {
|
||||
configRoot.getOptionalConfig('catalog.processors.ldapOrg');
|
||||
return new LdapOrgReaderProcessor({
|
||||
...options,
|
||||
providers: config ? readLdapConfig(config) : [],
|
||||
providers: config ? readLdapLegacyConfig(config) : [],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -15,5 +15,8 @@
|
||||
*/
|
||||
|
||||
export { LdapOrgEntityProvider } from './LdapOrgEntityProvider';
|
||||
export type { LdapOrgEntityProviderOptions } from './LdapOrgEntityProvider';
|
||||
export type {
|
||||
LdapOrgEntityProviderOptions,
|
||||
LdapOrgEntityProviderLegacyOptions,
|
||||
} from './LdapOrgEntityProvider';
|
||||
export { LdapOrgReaderProcessor } from './LdapOrgReaderProcessor';
|
||||
|
||||
Reference in New Issue
Block a user