@backstage-community/plugin-catalog-backend-module-keycloak 3.3.0 → 3.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  ### Dependencies
2
2
 
3
+ ## 3.4.1
4
+
5
+ ### Patch Changes
6
+
7
+ - f64bb64: Fixed the token update issue when the plugin uses the Keycloak client library. Added the ability to configure a maxConcurrency limit to control the number of parallel requests to the Keycloak server, preventing potential DoS attacks. Significantly improved the performance of parsing Keycloak user and group information.
8
+
9
+ ## 3.4.0
10
+
11
+ ### Minor Changes
12
+
13
+ - f7dfe8e: Update keycloak-admin-client to the latest version.
14
+
3
15
  ## 3.3.0
4
16
 
5
17
  ### Minor Changes
package/README.md CHANGED
@@ -117,17 +117,18 @@ Communication between Backstage and Keycloak is enabled by using the Keycloak AP
117
117
 
118
118
  The following table describes the parameters that you can configure to enable the plugin under `catalog.providers.keycloakOrg.<ENVIRONMENT_NAME>` object in the `app-config.yaml` file:
119
119
 
120
- | Name | Description | Default Value | Required |
121
- | ---------------- | ------------------------------------------------------------------ | ------------- | ---------------------------------------------------- |
122
- | `baseUrl` | Location of the Keycloak server, such as `https://localhost:8443`. | "" | Yes |
123
- | `realm` | Realm to synchronize | `master` | No |
124
- | `loginRealm` | Realm used to authenticate | `master` | No |
125
- | `username` | Username to authenticate | "" | Yes if using password based authentication |
126
- | `password` | Password to authenticate | "" | Yes if using password based authentication |
127
- | `clientId` | Client ID to authenticate | "" | Yes if using client credentials based authentication |
128
- | `clientSecret` | Client Secret to authenticate | "" | Yes if using client credentials based authentication |
129
- | `userQuerySize` | Number of users to query at a time | `100` | No |
130
- | `groupQuerySize` | Number of groups to query at a time | `100` | No |
120
+ | Name | Description | Default Value | Required |
121
+ | ---------------- | ------------------------------------------------------------------------- | ------------- | ---------------------------------------------------- |
122
+ | `baseUrl` | Location of the Keycloak server, such as `https://localhost:8443`. | "" | Yes |
123
+ | `realm` | Realm to synchronize | `master` | No |
124
+ | `loginRealm` | Realm used to authenticate | `master` | No |
125
+ | `username` | Username to authenticate | "" | Yes if using password based authentication |
126
+ | `password` | Password to authenticate | "" | Yes if using password based authentication |
127
+ | `clientId` | Client ID to authenticate | "" | Yes if using client credentials based authentication |
128
+ | `clientSecret` | Client Secret to authenticate | "" | Yes if using client credentials based authentication |
129
+ | `userQuerySize` | Number of users to query at a time | `100` | No |
130
+ | `groupQuerySize` | Number of groups to query at a time | `100` | No |
131
+ | `maxConcurrency` | Maximum request concurrency to prevent DoS attacks on the Keycloak server | `20` | No |
131
132
 
132
133
  When using client credentials, the access type must be set to `confidential` and service accounts must be enabled. You must also add the following roles from the `realm-management` client role:
133
134
 
package/config.d.ts CHANGED
@@ -51,6 +51,11 @@ export interface Config {
51
51
  * @see https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_groups_resource
52
52
  */
53
53
  groupQuerySize?: number;
54
+ /**
55
+ * Maximum request concurrency to prevent DoS attacks on the Keycloak server.
56
+ */
57
+ maxConcurrency?: number;
58
+
54
59
  schedule?: SchedulerServiceTaskScheduleDefinitionConfig;
55
60
  } & (
56
61
  | {
package/dist/index.d.ts CHANGED
@@ -8,22 +8,40 @@ import UserRepresentation from '@keycloak/keycloak-admin-client/lib/defs/userRep
8
8
 
9
9
  /**
10
10
  * @public
11
+ * The Keycloak group representation with parent and group members information.
11
12
  */
12
13
  interface GroupRepresentationWithParent extends GroupRepresentation {
14
+ /**
15
+ * The parent group ID.
16
+ */
13
17
  parentId?: string;
18
+ /**
19
+ * The parent group name.
20
+ */
14
21
  parent?: string;
22
+ /**
23
+ * The group members.
24
+ */
15
25
  members?: string[];
16
26
  }
17
27
  /**
18
28
  * @public
29
+ * The Keycloak group representation with parent, group members, and conrresponding backstage entity information.
19
30
  */
20
31
  interface GroupRepresentationWithParentAndEntity extends GroupRepresentationWithParent {
32
+ /**
33
+ * The corresponding backstage entity information.
34
+ */
21
35
  entity: GroupEntity;
22
36
  }
23
37
  /**
24
38
  * @public
39
+ * The Keycloak user representation with corresponding backstage entity information.
25
40
  */
26
41
  interface UserRepresentationWithEntity extends UserRepresentation {
42
+ /**
43
+ * The corresponding backstage entity information.
44
+ */
27
45
  entity: UserEntity;
28
46
  }
29
47
  /**
@@ -116,6 +134,10 @@ type KeycloakProviderConfig = {
116
134
  * @see https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_groups_resource
117
135
  */
118
136
  groupQuerySize?: number;
137
+ /**
138
+ * Maximum request concurrency to prevent DoS attacks on the Keycloak server.
139
+ */
140
+ maxConcurrency?: number;
119
141
  };
120
142
 
121
143
  /**
@@ -166,6 +188,12 @@ declare class KeycloakOrgEntityProvider implements EntityProvider {
166
188
  private options;
167
189
  private connection?;
168
190
  private scheduleFn?;
191
+ /**
192
+ * Static builder method to create multiple KeycloakOrgEntityProvider instances from a single config.
193
+ * @param deps - The dependencies required for the provider, including the configuration and logger.
194
+ * @param options - Options for scheduling tasks and transforming users and groups.
195
+ * @returns An array of KeycloakOrgEntityProvider instances.
196
+ */
169
197
  static fromConfig(deps: {
170
198
  config: Config;
171
199
  logger: LoggerService;
@@ -185,7 +213,14 @@ declare class KeycloakOrgEntityProvider implements EntityProvider {
185
213
  userTransformer?: UserTransformer;
186
214
  groupTransformer?: GroupTransformer;
187
215
  });
216
+ /**
217
+ * Returns the name of this entity provider.
218
+ */
188
219
  getProviderName(): string;
220
+ /**
221
+ * Connect to Backstage catalog entity provider
222
+ * @param connection - The connection to the catalog API ingestor, which allows the provision of new entities.
223
+ */
189
224
  connect(connection: EntityProviderConnection): Promise<void>;
190
225
  /**
191
226
  * Runs one complete ingestion loop. Call this method regularly at some
@@ -194,15 +229,21 @@ declare class KeycloakOrgEntityProvider implements EntityProvider {
194
229
  read(options?: {
195
230
  logger?: LoggerService;
196
231
  }): Promise<void>;
232
+ /**
233
+ * Periodically schedules a task to read Keycloak user and group information, parse it, and provision it to the Backstage catalog.
234
+ * @param taskRunner - The task runner to use for scheduling tasks.
235
+ */
197
236
  schedule(taskRunner: SchedulerServiceTaskRunner): void;
198
237
  }
199
238
 
200
239
  /**
201
240
  * @public
241
+ * Group transformer that does nothing.
202
242
  */
203
243
  declare const noopGroupTransformer: GroupTransformer;
204
244
  /**
205
245
  * @public
246
+ * User transformer that does nothing.
206
247
  */
207
248
  declare const noopUserTransformer: UserTransformer;
208
249
  /**
@@ -0,0 +1,62 @@
1
+ 'use strict';
2
+
3
+ var errors = require('@backstage/errors');
4
+ var jwt = require('jsonwebtoken');
5
+
6
+ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
7
+
8
+ var jwt__default = /*#__PURE__*/_interopDefaultCompat(jwt);
9
+
10
+ let refreshTokenPromise = null;
11
+ async function ensureTokenValid(kcAdminClient, provider, logger) {
12
+ if (!kcAdminClient.accessToken) {
13
+ await authenticate(kcAdminClient, provider, logger);
14
+ } else {
15
+ const decodedToken = jwt__default.default.decode(kcAdminClient.accessToken);
16
+ if (decodedToken && typeof decodedToken === "object" && decodedToken.exp) {
17
+ const tokenExpiry = decodedToken.exp * 1e3;
18
+ const now = Date.now();
19
+ if (now > tokenExpiry - 3e4) {
20
+ refreshTokenPromise = authenticate(
21
+ kcAdminClient,
22
+ provider,
23
+ logger
24
+ ).finally(() => {
25
+ refreshTokenPromise = null;
26
+ });
27
+ }
28
+ await refreshTokenPromise;
29
+ }
30
+ }
31
+ }
32
+ async function authenticate(kcAdminClient, provider, logger) {
33
+ try {
34
+ let credentials;
35
+ if (provider.username && provider.password) {
36
+ credentials = {
37
+ grantType: "password",
38
+ clientId: provider.clientId ?? "admin-cli",
39
+ username: provider.username,
40
+ password: provider.password
41
+ };
42
+ } else if (provider.clientId && provider.clientSecret) {
43
+ credentials = {
44
+ grantType: "client_credentials",
45
+ clientId: provider.clientId,
46
+ clientSecret: provider.clientSecret
47
+ };
48
+ } else {
49
+ throw new errors.InputError(
50
+ `username and password or clientId and clientSecret must be provided.`
51
+ );
52
+ }
53
+ await kcAdminClient.auth(credentials);
54
+ } catch (error) {
55
+ logger.error("Failed to authenticate", error.message);
56
+ throw error;
57
+ }
58
+ }
59
+
60
+ exports.authenticate = authenticate;
61
+ exports.ensureTokenValid = ensureTokenValid;
62
+ //# sourceMappingURL=authenticate.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authenticate.cjs.js","sources":["../../src/lib/authenticate.ts"],"sourcesContent":["/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport KeycloakAdminClient from '@keycloak/keycloak-admin-client';\nimport { KeycloakProviderConfig } from './config';\nimport { Credentials } from '@keycloak/keycloak-admin-client/lib/utils/auth';\nimport { InputError } from '@backstage/errors';\nimport { LoggerService } from '@backstage/backend-plugin-api';\nimport jwt from 'jsonwebtoken';\n\nlet refreshTokenPromise: Promise<void> | null = null;\n\nexport async function ensureTokenValid(\n kcAdminClient: KeycloakAdminClient,\n provider: KeycloakProviderConfig,\n logger: LoggerService,\n) {\n if (!kcAdminClient.accessToken) {\n await authenticate(kcAdminClient, provider, logger);\n } else {\n // returns null if token is not a JWT, string if payload is empty string, object if payload is a valid JSON\n const decodedToken = jwt.decode(kcAdminClient.accessToken);\n if (decodedToken && typeof decodedToken === 'object' && decodedToken.exp) {\n const tokenExpiry = decodedToken.exp * 1000; // Convert to milliseconds\n const now = Date.now();\n\n if (now > tokenExpiry - 30000) {\n refreshTokenPromise = authenticate(\n kcAdminClient,\n provider,\n logger,\n ).finally(() => {\n refreshTokenPromise = null;\n });\n }\n await refreshTokenPromise;\n }\n }\n}\n\nexport async function authenticate(\n kcAdminClient: KeycloakAdminClient,\n provider: KeycloakProviderConfig,\n logger: LoggerService,\n) {\n try {\n let credentials: Credentials;\n if (provider.username && provider.password) {\n credentials = {\n grantType: 'password',\n clientId: provider.clientId ?? 'admin-cli',\n username: provider.username,\n password: provider.password,\n };\n } else if (provider.clientId && provider.clientSecret) {\n credentials = {\n grantType: 'client_credentials',\n clientId: provider.clientId,\n clientSecret: provider.clientSecret,\n };\n } else {\n throw new InputError(\n `username and password or clientId and clientSecret must be provided.`,\n );\n }\n await kcAdminClient.auth(credentials);\n } catch (error) {\n logger.error('Failed to authenticate', error.message);\n throw error;\n }\n}\n"],"names":["jwt","InputError"],"mappings":";;;;;;;;;AAsBA,IAAI,mBAA4C,GAAA,IAAA;AAE1B,eAAA,gBAAA,CACpB,aACA,EAAA,QAAA,EACA,MACA,EAAA;AACA,EAAI,IAAA,CAAC,cAAc,WAAa,EAAA;AAC9B,IAAM,MAAA,YAAA,CAAa,aAAe,EAAA,QAAA,EAAU,MAAM,CAAA;AAAA,GAC7C,MAAA;AAEL,IAAA,MAAM,YAAe,GAAAA,oBAAA,CAAI,MAAO,CAAA,aAAA,CAAc,WAAW,CAAA;AACzD,IAAA,IAAI,YAAgB,IAAA,OAAO,YAAiB,KAAA,QAAA,IAAY,aAAa,GAAK,EAAA;AACxE,MAAM,MAAA,WAAA,GAAc,aAAa,GAAM,GAAA,GAAA;AACvC,MAAM,MAAA,GAAA,GAAM,KAAK,GAAI,EAAA;AAErB,MAAI,IAAA,GAAA,GAAM,cAAc,GAAO,EAAA;AAC7B,QAAsB,mBAAA,GAAA,YAAA;AAAA,UACpB,aAAA;AAAA,UACA,QAAA;AAAA,UACA;AAAA,SACF,CAAE,QAAQ,MAAM;AACd,UAAsB,mBAAA,GAAA,IAAA;AAAA,SACvB,CAAA;AAAA;AAEH,MAAM,MAAA,mBAAA;AAAA;AACR;AAEJ;AAEsB,eAAA,YAAA,CACpB,aACA,EAAA,QAAA,EACA,MACA,EAAA;AACA,EAAI,IAAA;AACF,IAAI,IAAA,WAAA;AACJ,IAAI,IAAA,QAAA,CAAS,QAAY,IAAA,QAAA,CAAS,QAAU,EAAA;AAC1C,MAAc,WAAA,GAAA;AAAA,QACZ,SAAW,EAAA,UAAA;AAAA,QACX,QAAA,EAAU,SAAS,QAAY,IAAA,WAAA;AAAA,QAC/B,UAAU,QAAS,CAAA,QAAA;AAAA,QACnB,UAAU,QAAS,CAAA;AAAA,OACrB;AAAA,KACS,MAAA,IAAA,QAAA,CAAS,QAAY,IAAA,QAAA,CAAS,YAAc,EAAA;AACrD,MAAc,WAAA,GAAA;AAAA,QACZ,SAAW,EAAA,oBAAA;AAAA,QACX,UAAU,QAAS,CAAA,QAAA;AAAA,QACnB,cAAc,QAAS,CAAA;AAAA,OACzB;AAAA,KACK,MAAA;AACL,MAAA,MAAM,IAAIC,iBAAA;AAAA,QACR,CAAA,oEAAA;AAAA,OACF;AAAA;AAEF,IAAM,MAAA,aAAA,CAAc,KAAK,WAAW,CAAA;AAAA,WAC7B,KAAO,EAAA;AACd,IAAO,MAAA,CAAA,KAAA,CAAM,wBAA0B,EAAA,KAAA,CAAM,OAAO,CAAA;AACpD,IAAM,MAAA,KAAA;AAAA;AAEV;;;;;"}
@@ -13,6 +13,7 @@ const readProviderConfig = (id, providerConfigInstance) => {
13
13
  const clientSecret = providerConfigInstance.getOptionalString("clientSecret");
14
14
  const userQuerySize = providerConfigInstance.getOptionalNumber("userQuerySize");
15
15
  const groupQuerySize = providerConfigInstance.getOptionalNumber("groupQuerySize");
16
+ const maxConcurrency = providerConfigInstance.getOptionalNumber("maxConcurrency");
16
17
  if (clientId && !clientSecret) {
17
18
  throw new errors.InputError(
18
19
  `clientSecret must be provided when clientId is defined.`
@@ -43,7 +44,8 @@ const readProviderConfig = (id, providerConfigInstance) => {
43
44
  clientSecret,
44
45
  schedule,
45
46
  userQuerySize,
46
- groupQuerySize
47
+ groupQuerySize,
48
+ maxConcurrency
47
49
  };
48
50
  };
49
51
  const readProviderConfigs = (config) => {
@@ -1 +1 @@
1
- {"version":3,"file":"config.cjs.js","sources":["../../src/lib/config.ts"],"sourcesContent":["/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { readSchedulerServiceTaskScheduleDefinitionFromConfig } from '@backstage/backend-plugin-api';\nimport type { SchedulerServiceTaskScheduleDefinition } from '@backstage/backend-plugin-api';\nimport type { Config } from '@backstage/config';\nimport { InputError } from '@backstage/errors';\n\n/**\n * The configuration parameters for a single Keycloak provider.\n *\n * @public\n */\nexport type KeycloakProviderConfig = {\n /**\n * Identifier of the provider which will be used i.e. at the location key for ingested entities.\n */\n id: string;\n\n /**\n * The Keycloak base URL\n */\n baseUrl: string;\n\n /**\n * The username to use for authenticating requests\n * If specified, password must also be specified\n */\n username?: string;\n\n /**\n * The password to use for authenticating requests\n * If specified, username must also be specified\n */\n password?: string;\n\n /**\n * The clientId to use for authenticating requests\n * If specified, clientSecret must also be specified\n */\n clientId?: string;\n\n /**\n * The clientSecret to use for authenticating requests\n * If specified, clientId must also be specified\n */\n clientSecret?: string;\n\n /**\n * name of the Keycloak realm\n */\n realm: string;\n\n /**\n * name of the Keycloak login realm\n */\n loginRealm?: string;\n\n /**\n * Schedule configuration for refresh tasks.\n */\n schedule?: SchedulerServiceTaskScheduleDefinition;\n\n /**\n * The number of users to query at a time.\n * @defaultValue 100\n * @remarks\n * This is a performance optimization to avoid querying too many users at once.\n * @see https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_users_resource\n */\n userQuerySize?: number;\n\n /**\n * The number of groups to query at a time.\n * @defaultValue 100\n * @remarks\n * This is a performance optimization to avoid querying too many groups at once.\n * @see https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_groups_resource\n */\n groupQuerySize?: number;\n};\n\nconst readProviderConfig = (\n id: string,\n providerConfigInstance: Config,\n): KeycloakProviderConfig => {\n const baseUrl = providerConfigInstance.getString('baseUrl');\n const realm = providerConfigInstance.getOptionalString('realm') ?? 'master';\n const loginRealm =\n providerConfigInstance.getOptionalString('loginRealm') ?? 'master';\n const username = providerConfigInstance.getOptionalString('username');\n const password = providerConfigInstance.getOptionalString('password');\n const clientId = providerConfigInstance.getOptionalString('clientId');\n const clientSecret = providerConfigInstance.getOptionalString('clientSecret');\n const userQuerySize =\n providerConfigInstance.getOptionalNumber('userQuerySize');\n const groupQuerySize =\n providerConfigInstance.getOptionalNumber('groupQuerySize');\n\n if (clientId && !clientSecret) {\n throw new InputError(\n `clientSecret must be provided when clientId is defined.`,\n );\n }\n\n if (clientSecret && !clientId) {\n throw new InputError(\n `clientId must be provided when clientSecret is defined.`,\n );\n }\n\n if (username && !password) {\n throw new InputError(`password must be provided when username is defined.`);\n }\n\n if (password && !username) {\n throw new InputError(`username must be provided when password is defined.`);\n }\n\n const schedule = providerConfigInstance.has('schedule')\n ? readSchedulerServiceTaskScheduleDefinitionFromConfig(\n providerConfigInstance.getConfig('schedule'),\n )\n : undefined;\n\n return {\n id,\n baseUrl,\n loginRealm,\n realm,\n username,\n password,\n clientId,\n clientSecret,\n schedule,\n userQuerySize,\n groupQuerySize,\n };\n};\n\nexport const readProviderConfigs = (\n config: Config,\n): KeycloakProviderConfig[] => {\n const providersConfig = config.getOptionalConfig(\n 'catalog.providers.keycloakOrg',\n );\n if (!providersConfig) {\n return [];\n }\n return providersConfig.keys().map(id => {\n const providerConfigInstance = providersConfig.getConfig(id);\n return readProviderConfig(id, providerConfigInstance);\n });\n};\n"],"names":["InputError","readSchedulerServiceTaskScheduleDefinitionFromConfig"],"mappings":";;;;;AA+FA,MAAM,kBAAA,GAAqB,CACzB,EAAA,EACA,sBAC2B,KAAA;AAC3B,EAAM,MAAA,OAAA,GAAU,sBAAuB,CAAA,SAAA,CAAU,SAAS,CAAA;AAC1D,EAAA,MAAM,KAAQ,GAAA,sBAAA,CAAuB,iBAAkB,CAAA,OAAO,CAAK,IAAA,QAAA;AACnE,EAAA,MAAM,UACJ,GAAA,sBAAA,CAAuB,iBAAkB,CAAA,YAAY,CAAK,IAAA,QAAA;AAC5D,EAAM,MAAA,QAAA,GAAW,sBAAuB,CAAA,iBAAA,CAAkB,UAAU,CAAA;AACpE,EAAM,MAAA,QAAA,GAAW,sBAAuB,CAAA,iBAAA,CAAkB,UAAU,CAAA;AACpE,EAAM,MAAA,QAAA,GAAW,sBAAuB,CAAA,iBAAA,CAAkB,UAAU,CAAA;AACpE,EAAM,MAAA,YAAA,GAAe,sBAAuB,CAAA,iBAAA,CAAkB,cAAc,CAAA;AAC5E,EAAM,MAAA,aAAA,GACJ,sBAAuB,CAAA,iBAAA,CAAkB,eAAe,CAAA;AAC1D,EAAM,MAAA,cAAA,GACJ,sBAAuB,CAAA,iBAAA,CAAkB,gBAAgB,CAAA;AAE3D,EAAI,IAAA,QAAA,IAAY,CAAC,YAAc,EAAA;AAC7B,IAAA,MAAM,IAAIA,iBAAA;AAAA,MACR,CAAA,uDAAA;AAAA,KACF;AAAA;AAGF,EAAI,IAAA,YAAA,IAAgB,CAAC,QAAU,EAAA;AAC7B,IAAA,MAAM,IAAIA,iBAAA;AAAA,MACR,CAAA,uDAAA;AAAA,KACF;AAAA;AAGF,EAAI,IAAA,QAAA,IAAY,CAAC,QAAU,EAAA;AACzB,IAAM,MAAA,IAAIA,kBAAW,CAAqD,mDAAA,CAAA,CAAA;AAAA;AAG5E,EAAI,IAAA,QAAA,IAAY,CAAC,QAAU,EAAA;AACzB,IAAM,MAAA,IAAIA,kBAAW,CAAqD,mDAAA,CAAA,CAAA;AAAA;AAG5E,EAAA,MAAM,QAAW,GAAA,sBAAA,CAAuB,GAAI,CAAA,UAAU,CAClD,GAAAC,qEAAA;AAAA,IACE,sBAAA,CAAuB,UAAU,UAAU;AAAA,GAE7C,GAAA,SAAA;AAEJ,EAAO,OAAA;AAAA,IACL,EAAA;AAAA,IACA,OAAA;AAAA,IACA,UAAA;AAAA,IACA,KAAA;AAAA,IACA,QAAA;AAAA,IACA,QAAA;AAAA,IACA,QAAA;AAAA,IACA,YAAA;AAAA,IACA,QAAA;AAAA,IACA,aAAA;AAAA,IACA;AAAA,GACF;AACF,CAAA;AAEa,MAAA,mBAAA,GAAsB,CACjC,MAC6B,KAAA;AAC7B,EAAA,MAAM,kBAAkB,MAAO,CAAA,iBAAA;AAAA,IAC7B;AAAA,GACF;AACA,EAAA,IAAI,CAAC,eAAiB,EAAA;AACpB,IAAA,OAAO,EAAC;AAAA;AAEV,EAAA,OAAO,eAAgB,CAAA,IAAA,EAAO,CAAA,GAAA,CAAI,CAAM,EAAA,KAAA;AACtC,IAAM,MAAA,sBAAA,GAAyB,eAAgB,CAAA,SAAA,CAAU,EAAE,CAAA;AAC3D,IAAO,OAAA,kBAAA,CAAmB,IAAI,sBAAsB,CAAA;AAAA,GACrD,CAAA;AACH;;;;"}
1
+ {"version":3,"file":"config.cjs.js","sources":["../../src/lib/config.ts"],"sourcesContent":["/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { readSchedulerServiceTaskScheduleDefinitionFromConfig } from '@backstage/backend-plugin-api';\nimport type { SchedulerServiceTaskScheduleDefinition } from '@backstage/backend-plugin-api';\nimport type { Config } from '@backstage/config';\nimport { InputError } from '@backstage/errors';\n\n/**\n * The configuration parameters for a single Keycloak provider.\n *\n * @public\n */\nexport type KeycloakProviderConfig = {\n /**\n * Identifier of the provider which will be used i.e. at the location key for ingested entities.\n */\n id: string;\n\n /**\n * The Keycloak base URL\n */\n baseUrl: string;\n\n /**\n * The username to use for authenticating requests\n * If specified, password must also be specified\n */\n username?: string;\n\n /**\n * The password to use for authenticating requests\n * If specified, username must also be specified\n */\n password?: string;\n\n /**\n * The clientId to use for authenticating requests\n * If specified, clientSecret must also be specified\n */\n clientId?: string;\n\n /**\n * The clientSecret to use for authenticating requests\n * If specified, clientId must also be specified\n */\n clientSecret?: string;\n\n /**\n * name of the Keycloak realm\n */\n realm: string;\n\n /**\n * name of the Keycloak login realm\n */\n loginRealm?: string;\n\n /**\n * Schedule configuration for refresh tasks.\n */\n schedule?: SchedulerServiceTaskScheduleDefinition;\n\n /**\n * The number of users to query at a time.\n * @defaultValue 100\n * @remarks\n * This is a performance optimization to avoid querying too many users at once.\n * @see https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_users_resource\n */\n userQuerySize?: number;\n\n /**\n * The number of groups to query at a time.\n * @defaultValue 100\n * @remarks\n * This is a performance optimization to avoid querying too many groups at once.\n * @see https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_groups_resource\n */\n groupQuerySize?: number;\n\n /**\n * Maximum request concurrency to prevent DoS attacks on the Keycloak server.\n */\n maxConcurrency?: number;\n};\n\nconst readProviderConfig = (\n id: string,\n providerConfigInstance: Config,\n): KeycloakProviderConfig => {\n const baseUrl = providerConfigInstance.getString('baseUrl');\n const realm = providerConfigInstance.getOptionalString('realm') ?? 'master';\n const loginRealm =\n providerConfigInstance.getOptionalString('loginRealm') ?? 'master';\n const username = providerConfigInstance.getOptionalString('username');\n const password = providerConfigInstance.getOptionalString('password');\n const clientId = providerConfigInstance.getOptionalString('clientId');\n const clientSecret = providerConfigInstance.getOptionalString('clientSecret');\n const userQuerySize =\n providerConfigInstance.getOptionalNumber('userQuerySize');\n const groupQuerySize =\n providerConfigInstance.getOptionalNumber('groupQuerySize');\n const maxConcurrency =\n providerConfigInstance.getOptionalNumber('maxConcurrency');\n\n if (clientId && !clientSecret) {\n throw new InputError(\n `clientSecret must be provided when clientId is defined.`,\n );\n }\n\n if (clientSecret && !clientId) {\n throw new InputError(\n `clientId must be provided when clientSecret is defined.`,\n );\n }\n\n if (username && !password) {\n throw new InputError(`password must be provided when username is defined.`);\n }\n\n if (password && !username) {\n throw new InputError(`username must be provided when password is defined.`);\n }\n\n const schedule = providerConfigInstance.has('schedule')\n ? readSchedulerServiceTaskScheduleDefinitionFromConfig(\n providerConfigInstance.getConfig('schedule'),\n )\n : undefined;\n\n return {\n id,\n baseUrl,\n loginRealm,\n realm,\n username,\n password,\n clientId,\n clientSecret,\n schedule,\n userQuerySize,\n groupQuerySize,\n maxConcurrency,\n };\n};\n\nexport const readProviderConfigs = (\n config: Config,\n): KeycloakProviderConfig[] => {\n const providersConfig = config.getOptionalConfig(\n 'catalog.providers.keycloakOrg',\n );\n if (!providersConfig) {\n return [];\n }\n return providersConfig.keys().map(id => {\n const providerConfigInstance = providersConfig.getConfig(id);\n return readProviderConfig(id, providerConfigInstance);\n });\n};\n"],"names":["InputError","readSchedulerServiceTaskScheduleDefinitionFromConfig"],"mappings":";;;;;AAoGA,MAAM,kBAAA,GAAqB,CACzB,EAAA,EACA,sBAC2B,KAAA;AAC3B,EAAM,MAAA,OAAA,GAAU,sBAAuB,CAAA,SAAA,CAAU,SAAS,CAAA;AAC1D,EAAA,MAAM,KAAQ,GAAA,sBAAA,CAAuB,iBAAkB,CAAA,OAAO,CAAK,IAAA,QAAA;AACnE,EAAA,MAAM,UACJ,GAAA,sBAAA,CAAuB,iBAAkB,CAAA,YAAY,CAAK,IAAA,QAAA;AAC5D,EAAM,MAAA,QAAA,GAAW,sBAAuB,CAAA,iBAAA,CAAkB,UAAU,CAAA;AACpE,EAAM,MAAA,QAAA,GAAW,sBAAuB,CAAA,iBAAA,CAAkB,UAAU,CAAA;AACpE,EAAM,MAAA,QAAA,GAAW,sBAAuB,CAAA,iBAAA,CAAkB,UAAU,CAAA;AACpE,EAAM,MAAA,YAAA,GAAe,sBAAuB,CAAA,iBAAA,CAAkB,cAAc,CAAA;AAC5E,EAAM,MAAA,aAAA,GACJ,sBAAuB,CAAA,iBAAA,CAAkB,eAAe,CAAA;AAC1D,EAAM,MAAA,cAAA,GACJ,sBAAuB,CAAA,iBAAA,CAAkB,gBAAgB,CAAA;AAC3D,EAAM,MAAA,cAAA,GACJ,sBAAuB,CAAA,iBAAA,CAAkB,gBAAgB,CAAA;AAE3D,EAAI,IAAA,QAAA,IAAY,CAAC,YAAc,EAAA;AAC7B,IAAA,MAAM,IAAIA,iBAAA;AAAA,MACR,CAAA,uDAAA;AAAA,KACF;AAAA;AAGF,EAAI,IAAA,YAAA,IAAgB,CAAC,QAAU,EAAA;AAC7B,IAAA,MAAM,IAAIA,iBAAA;AAAA,MACR,CAAA,uDAAA;AAAA,KACF;AAAA;AAGF,EAAI,IAAA,QAAA,IAAY,CAAC,QAAU,EAAA;AACzB,IAAM,MAAA,IAAIA,kBAAW,CAAqD,mDAAA,CAAA,CAAA;AAAA;AAG5E,EAAI,IAAA,QAAA,IAAY,CAAC,QAAU,EAAA;AACzB,IAAM,MAAA,IAAIA,kBAAW,CAAqD,mDAAA,CAAA,CAAA;AAAA;AAG5E,EAAA,MAAM,QAAW,GAAA,sBAAA,CAAuB,GAAI,CAAA,UAAU,CAClD,GAAAC,qEAAA;AAAA,IACE,sBAAA,CAAuB,UAAU,UAAU;AAAA,GAE7C,GAAA,SAAA;AAEJ,EAAO,OAAA;AAAA,IACL,EAAA;AAAA,IACA,OAAA;AAAA,IACA,UAAA;AAAA,IACA,KAAA;AAAA,IACA,QAAA;AAAA,IACA,QAAA;AAAA,IACA,QAAA;AAAA,IACA,YAAA;AAAA,IACA,QAAA;AAAA,IACA,aAAA;AAAA,IACA,cAAA;AAAA,IACA;AAAA,GACF;AACF,CAAA;AAEa,MAAA,mBAAA,GAAsB,CACjC,MAC6B,KAAA;AAC7B,EAAA,MAAM,kBAAkB,MAAO,CAAA,iBAAA;AAAA,IAC7B;AAAA,GACF;AACA,EAAA,IAAI,CAAC,eAAiB,EAAA;AACpB,IAAA,OAAO,EAAC;AAAA;AAEV,EAAA,OAAO,eAAgB,CAAA,IAAA,EAAO,CAAA,GAAA,CAAI,CAAM,EAAA,KAAA;AACtC,IAAM,MAAA,sBAAA,GAAyB,eAAgB,CAAA,SAAA,CAAU,EAAE,CAAA;AAC3D,IAAO,OAAA,kBAAA,CAAmB,IAAI,sBAAsB,CAAA;AAAA,GACrD,CAAA;AACH;;;;"}
@@ -2,6 +2,7 @@
2
2
 
3
3
  var constants = require('./constants.cjs.js');
4
4
  var transformers = require('./transformers.cjs.js');
5
+ var authenticate = require('./authenticate.cjs.js');
5
6
 
6
7
  const parseGroup = async (keycloakGroup, realm, groupTransformer) => {
7
8
  const transformer = groupTransformer ?? transformers.noopGroupTransformer;
@@ -28,7 +29,7 @@ const parseGroup = async (keycloakGroup, realm, groupTransformer) => {
28
29
  };
29
30
  return await transformer(entity, keycloakGroup, realm);
30
31
  };
31
- const parseUser = async (user, realm, keycloakGroups, userTransformer) => {
32
+ const parseUser = async (user, realm, keycloakGroups, groupIndex, userTransformer) => {
32
33
  const transformer = userTransformer ?? transformers.noopUserTransformer;
33
34
  const entity = {
34
35
  apiVersion: "backstage.io/v1beta1",
@@ -47,34 +48,46 @@ const parseUser = async (user, realm, keycloakGroups, userTransformer) => {
47
48
  displayName: [user.firstName, user.lastName].filter(Boolean).join(" ")
48
49
  } : {}
49
50
  },
50
- memberOf: keycloakGroups.filter((g) => g.members?.includes(user.username)).map((g) => g.entity.metadata.name)
51
+ memberOf: groupIndex.get(user.username) ?? []
51
52
  }
52
53
  };
53
54
  return await transformer(entity, user, realm, keycloakGroups);
54
55
  };
55
- async function getEntities(entities, config, logger, entityQuerySize = constants.KEYCLOAK_ENTITY_QUERY_SIZE) {
56
- const rawEntityCount = await entities.count({ realm: config.realm });
56
+ async function getEntities(getEntitiesFn, config, logger, limit, entityQuerySize = constants.KEYCLOAK_ENTITY_QUERY_SIZE) {
57
+ const entitiesAPI = await getEntitiesFn();
58
+ const rawEntityCount = await entitiesAPI.count({ realm: config.realm });
57
59
  const entityCount = typeof rawEntityCount === "number" ? rawEntityCount : rawEntityCount.count;
58
60
  const pageCount = Math.ceil(entityCount / entityQuerySize);
59
61
  const entityPromises = Array.from(
60
62
  { length: pageCount },
61
- (_, i) => entities.find({
62
- realm: config.realm,
63
- max: entityQuerySize,
64
- first: i * entityQuerySize
65
- }).catch(
66
- (err) => logger.warn("Failed to retieve Keycloak entities.", err)
63
+ (_, i) => limit(
64
+ () => getEntitiesFn().then((entities) => {
65
+ return entities.find({
66
+ realm: config.realm,
67
+ max: entityQuerySize,
68
+ first: i * entityQuerySize
69
+ }).then((ents) => {
70
+ logger.debug(
71
+ `Importing keycloak entities batch with index ${i} from pages: ${pageCount}`
72
+ );
73
+ return ents;
74
+ }).catch((err) => {
75
+ logger.warn("Failed to retieve Keycloak entities.", err);
76
+ return [];
77
+ });
78
+ })
67
79
  )
68
80
  );
69
81
  const entityResults = (await Promise.all(entityPromises)).flat();
70
82
  return entityResults;
71
83
  }
72
- async function getAllGroupMembers(groups, groupId, config, options) {
84
+ async function getAllGroupMembers(groupsAPI, groupId, config, options) {
73
85
  const querySize = options?.userQuerySize || 100;
74
86
  let allMembers = [];
75
87
  let page = 0;
76
88
  let totalMembers = 0;
77
89
  do {
90
+ const groups = await groupsAPI();
78
91
  const members = await groups.listMembers({
79
92
  id: groupId,
80
93
  max: querySize,
@@ -91,22 +104,23 @@ async function getAllGroupMembers(groups, groupId, config, options) {
91
104
  } while (totalMembers > 0);
92
105
  return allMembers;
93
106
  }
94
- async function processGroupsRecursively(topLevelGroups, entities, realm) {
107
+ async function processGroupsRecursively(kcAdminClient, config, logger, topLevelGroups) {
95
108
  const allGroups = [];
96
109
  for (const group of topLevelGroups) {
97
110
  allGroups.push(group);
98
111
  if (group.subGroupCount > 0) {
99
- const subgroups = await entities.listSubGroups({
112
+ await authenticate.ensureTokenValid(kcAdminClient, config, logger);
113
+ const subgroups = await kcAdminClient.groups.listSubGroups({
100
114
  parentId: group.id,
101
115
  first: 0,
102
116
  max: group.subGroupCount,
103
- briefRepresentation: true,
104
- realm
117
+ briefRepresentation: true
105
118
  });
106
119
  const subGroupResults = await processGroupsRecursively(
107
- subgroups,
108
- entities,
109
- realm
120
+ kcAdminClient,
121
+ config,
122
+ logger,
123
+ subgroups
110
124
  );
111
125
  allGroups.push(...subGroupResults);
112
126
  }
@@ -120,21 +134,32 @@ function* traverseGroups(group) {
120
134
  yield* traverseGroups(g);
121
135
  }
122
136
  }
123
- const readKeycloakRealm = async (client, config, logger, options) => {
137
+ const readKeycloakRealm = async (client, config, logger, limit, options) => {
124
138
  const kUsers = await getEntities(
125
- client.users,
139
+ async () => {
140
+ await authenticate.ensureTokenValid(client, config, logger);
141
+ return client.users;
142
+ },
126
143
  config,
127
144
  logger,
145
+ limit,
128
146
  options?.userQuerySize
129
147
  );
148
+ logger.debug(`Fetched ${kUsers.length} users from Keycloak`);
130
149
  const topLevelKGroups = await getEntities(
131
- client.groups,
150
+ async () => {
151
+ await authenticate.ensureTokenValid(client, config, logger);
152
+ return client.groups;
153
+ },
132
154
  config,
133
155
  logger,
156
+ limit,
134
157
  options?.groupQuerySize
135
158
  );
159
+ logger.debug(`Fetched ${topLevelKGroups.length} groups from Keycloak`);
136
160
  let serverVersion;
137
161
  try {
162
+ await authenticate.ensureTokenValid(client, config, logger);
138
163
  const serverInfo = await client.serverInfo.find();
139
164
  serverVersion = parseInt(
140
165
  serverInfo.systemInfo?.version?.slice(0, 2) || "",
@@ -145,11 +170,13 @@ const readKeycloakRealm = async (client, config, logger, options) => {
145
170
  }
146
171
  const isVersion23orHigher = serverVersion >= 23;
147
172
  let rawKGroups = [];
173
+ logger.debug(`Processing groups recursively`);
148
174
  if (isVersion23orHigher) {
149
175
  rawKGroups = await processGroupsRecursively(
150
- topLevelKGroups,
151
- client.groups,
152
- config.realm
176
+ client,
177
+ config,
178
+ logger,
179
+ topLevelKGroups
153
180
  );
154
181
  } else {
155
182
  rawKGroups = topLevelKGroups.reduce(
@@ -157,77 +184,114 @@ const readKeycloakRealm = async (client, config, logger, options) => {
157
184
  []
158
185
  );
159
186
  }
187
+ logger.debug(`Fetching group members for keycloak groups and list subgroups`);
160
188
  const kGroups = await Promise.all(
161
- rawKGroups.map(async (g) => {
162
- g.members = await getAllGroupMembers(
163
- client.groups,
164
- g.id,
165
- config,
166
- options
167
- );
168
- if (isVersion23orHigher) {
169
- if (g.subGroupCount > 0) {
170
- g.subGroups = await client.groups.listSubGroups({
171
- parentId: g.id,
172
- first: 0,
173
- max: g.subGroupCount,
174
- briefRepresentation: false,
175
- realm: config.realm
176
- });
177
- }
178
- if (g.parentId) {
179
- const groupParent = await client.groups.findOne({
180
- id: g.parentId,
181
- realm: config.realm
182
- });
183
- g.parent = groupParent?.name;
189
+ rawKGroups.map(
190
+ (g) => limit(async () => {
191
+ g.members = await getAllGroupMembers(
192
+ async () => {
193
+ await authenticate.ensureTokenValid(client, config, logger);
194
+ return client.groups;
195
+ },
196
+ g.id,
197
+ config,
198
+ options
199
+ );
200
+ if (isVersion23orHigher) {
201
+ if (g.subGroupCount > 0) {
202
+ await authenticate.ensureTokenValid(client, config, logger);
203
+ g.subGroups = await client.groups.listSubGroups({
204
+ parentId: g.id,
205
+ first: 0,
206
+ max: g.subGroupCount,
207
+ briefRepresentation: false,
208
+ realm: config.realm
209
+ });
210
+ }
211
+ if (g.parentId) {
212
+ await authenticate.ensureTokenValid(client, config, logger);
213
+ const groupParent = await client.groups.findOne({
214
+ id: g.parentId,
215
+ realm: config.realm
216
+ });
217
+ g.parent = groupParent?.name;
218
+ }
184
219
  }
220
+ return g;
221
+ })
222
+ )
223
+ );
224
+ logger.debug(`Parsing groups`);
225
+ const parsedGroups = await Promise.all(
226
+ kGroups.map(async (g) => {
227
+ if (!g) {
228
+ return null;
229
+ }
230
+ const entity = await parseGroup(
231
+ g,
232
+ config.realm,
233
+ options?.groupTransformer
234
+ );
235
+ if (entity) {
236
+ return { ...g, entity };
185
237
  }
186
- return g;
238
+ return null;
187
239
  })
188
240
  );
189
- const parsedGroups = await kGroups.reduce(async (promise, g) => {
190
- const partial = await promise;
191
- const entity = await parseGroup(g, config.realm, options?.groupTransformer);
192
- if (entity) {
193
- const group = {
194
- ...g,
195
- entity
196
- };
197
- partial.push(group);
198
- }
199
- return partial;
200
- }, Promise.resolve([]));
201
- const parsedUsers = await kUsers.reduce(async (promise, u) => {
202
- const partial = await promise;
203
- const entity = await parseUser(
204
- u,
205
- config.realm,
206
- parsedGroups,
207
- options?.userTransformer
208
- );
209
- if (entity) {
210
- const user = { ...u, entity };
211
- partial.push(user);
241
+ const filteredParsedGroups = parsedGroups.filter(
242
+ (group) => group !== null
243
+ );
244
+ const groupIndex = /* @__PURE__ */ new Map();
245
+ filteredParsedGroups.forEach((group) => {
246
+ if (group.members) {
247
+ group.members.forEach((member) => {
248
+ if (!groupIndex.has(member)) {
249
+ groupIndex.set(member, []);
250
+ }
251
+ groupIndex.get(member)?.push(group.entity.metadata.name);
252
+ });
212
253
  }
213
- return partial;
214
- }, Promise.resolve([]));
215
- const groups = parsedGroups.map((g) => {
254
+ });
255
+ logger.debug("Parsing users");
256
+ const parsedUsers = await Promise.all(
257
+ kUsers.map(async (u) => {
258
+ if (!u) {
259
+ return null;
260
+ }
261
+ const entity = await parseUser(
262
+ u,
263
+ config.realm,
264
+ filteredParsedGroups,
265
+ groupIndex,
266
+ options?.userTransformer
267
+ );
268
+ if (entity) {
269
+ return { ...u, entity };
270
+ }
271
+ return null;
272
+ })
273
+ );
274
+ const filteredParsedUsers = parsedUsers.filter(
275
+ (user) => user !== null
276
+ );
277
+ logger.debug(`Set up group members and children information`);
278
+ const userMap = new Map(
279
+ filteredParsedUsers.map((user) => [user.username, user.entity.metadata.name])
280
+ );
281
+ const groupMap = new Map(
282
+ filteredParsedGroups.map((group) => [group.name, group.entity.metadata.name])
283
+ );
284
+ const groups = filteredParsedGroups.map((g) => {
216
285
  const entity = g.entity;
217
- entity.spec.members = g.entity.spec.members?.flatMap((m) => {
218
- const name = parsedUsers.find((p) => p.username === m)?.entity.metadata.name;
219
- return name ? [name] : [];
220
- }) ?? [];
221
- entity.spec.children = g.entity.spec.children?.flatMap((c) => {
222
- const child = parsedGroups.find((p) => p.name === c)?.entity.metadata.name;
223
- return child ? [child] : [];
224
- }) ?? [];
225
- entity.spec.parent = parsedGroups.find(
226
- (p) => p.name === entity.spec.parent
227
- )?.entity.metadata.name;
286
+ entity.spec.members = g.entity.spec.members?.flatMap((m) => userMap.get(m) ?? []) ?? [];
287
+ entity.spec.children = g.entity.spec.children?.flatMap((c) => groupMap.get(c) ?? []) ?? [];
288
+ entity.spec.parent = groupMap.get(entity.spec.parent);
228
289
  return entity;
229
290
  });
230
- return { users: parsedUsers.map((u) => u.entity), groups };
291
+ logger.info(
292
+ `Prepared to ingest ${parsedUsers.length} users and ${groups.length} groups into the catalog from Keycloak`
293
+ );
294
+ return { users: filteredParsedUsers.map((u) => u.entity), groups };
231
295
  };
232
296
 
233
297
  exports.getEntities = getEntities;
@@ -1 +1 @@
1
- {"version":3,"file":"read.cjs.js","sources":["../../src/lib/read.ts"],"sourcesContent":["/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { LoggerService } from '@backstage/backend-plugin-api';\nimport type { GroupEntity, UserEntity } from '@backstage/catalog-model';\n\nimport type KeycloakAdminClient from '@keycloak/keycloak-admin-client';\nimport type GroupRepresentation from '@keycloak/keycloak-admin-client/lib/defs/groupRepresentation';\nimport type UserRepresentation from '@keycloak/keycloak-admin-client/lib/defs/userRepresentation';\nimport type { Groups } from '@keycloak/keycloak-admin-client/lib/resources/groups';\nimport type { Users } from '@keycloak/keycloak-admin-client/lib/resources/users';\n\nimport { KeycloakProviderConfig } from './config';\nimport {\n KEYCLOAK_ENTITY_QUERY_SIZE,\n KEYCLOAK_ID_ANNOTATION,\n KEYCLOAK_REALM_ANNOTATION,\n} from './constants';\nimport { noopGroupTransformer, noopUserTransformer } from './transformers';\nimport {\n GroupRepresentationWithParent,\n GroupRepresentationWithParentAndEntity,\n GroupTransformer,\n UserRepresentationWithEntity,\n UserTransformer,\n} from './types';\n\nexport const parseGroup = async (\n keycloakGroup: GroupRepresentationWithParent,\n realm: string,\n groupTransformer?: GroupTransformer,\n): Promise<GroupEntity | undefined> => {\n const transformer = groupTransformer ?? noopGroupTransformer;\n const entity: GroupEntity = {\n apiVersion: 'backstage.io/v1beta1',\n kind: 'Group',\n metadata: {\n name: keycloakGroup.name!,\n annotations: {\n [KEYCLOAK_ID_ANNOTATION]: keycloakGroup.id!,\n [KEYCLOAK_REALM_ANNOTATION]: realm,\n },\n },\n spec: {\n type: 'group',\n profile: {\n displayName: keycloakGroup.name!,\n },\n // children, parent and members are updated again after all group and user transformers applied.\n children: keycloakGroup.subGroups?.map(g => g.name!) ?? [],\n parent: keycloakGroup.parent,\n members: keycloakGroup.members,\n },\n };\n\n return await transformer(entity, keycloakGroup, realm);\n};\n\nexport const parseUser = async (\n user: UserRepresentation,\n realm: string,\n keycloakGroups: GroupRepresentationWithParentAndEntity[],\n\n userTransformer?: UserTransformer,\n): Promise<UserEntity | undefined> => {\n const transformer = userTransformer ?? noopUserTransformer;\n const entity: UserEntity = {\n apiVersion: 'backstage.io/v1beta1',\n kind: 'User',\n metadata: {\n name: user.username!,\n annotations: {\n [KEYCLOAK_ID_ANNOTATION]: user.id!,\n [KEYCLOAK_REALM_ANNOTATION]: realm,\n },\n },\n spec: {\n profile: {\n email: user.email,\n ...(user.firstName || user.lastName\n ? {\n displayName: [user.firstName, user.lastName]\n .filter(Boolean)\n .join(' '),\n }\n : {}),\n },\n memberOf: keycloakGroups\n .filter(g => g.members?.includes(user.username!))\n .map(g => g.entity.metadata.name),\n },\n };\n\n return await transformer(entity, user, realm, keycloakGroups);\n};\n\nexport async function getEntities<T extends Users | Groups>(\n entities: T,\n config: KeycloakProviderConfig,\n logger: LoggerService,\n entityQuerySize: number = KEYCLOAK_ENTITY_QUERY_SIZE,\n): Promise<Awaited<ReturnType<T['find']>>> {\n const rawEntityCount = await entities.count({ realm: config.realm });\n const entityCount =\n typeof rawEntityCount === 'number' ? rawEntityCount : rawEntityCount.count;\n\n const pageCount = Math.ceil(entityCount / entityQuerySize);\n\n // The next line acts like range in python\n const entityPromises = Array.from(\n { length: pageCount },\n (_, i) =>\n entities\n .find({\n realm: config.realm,\n max: entityQuerySize,\n first: i * entityQuerySize,\n })\n .catch(err =>\n logger.warn('Failed to retieve Keycloak entities.', err),\n ) as ReturnType<T['find']>,\n );\n\n const entityResults = (await Promise.all(entityPromises)).flat() as Awaited<\n ReturnType<T['find']>\n >;\n\n return entityResults;\n}\n\nasync function getAllGroupMembers<T extends Groups>(\n groups: T,\n groupId: string,\n config: KeycloakProviderConfig,\n options?: { userQuerySize?: number },\n): Promise<string[]> {\n const querySize = options?.userQuerySize || 100;\n\n let allMembers: string[] = [];\n let page = 0;\n let totalMembers = 0;\n\n do {\n const members = await groups.listMembers({\n id: groupId,\n max: querySize,\n realm: config.realm,\n first: page * querySize,\n });\n\n if (members.length > 0) {\n allMembers = allMembers.concat(members.map(m => m.username!));\n totalMembers = members.length; // Get the number of members retrieved\n } else {\n totalMembers = 0; // No members retrieved\n }\n\n page++;\n } while (totalMembers > 0);\n\n return allMembers;\n}\n\nexport async function processGroupsRecursively(\n topLevelGroups: GroupRepresentationWithParent[],\n entities: Groups,\n realm: string,\n) {\n const allGroups: GroupRepresentationWithParent[] = [];\n for (const group of topLevelGroups) {\n allGroups.push(group);\n\n if (group.subGroupCount! > 0) {\n const subgroups = await entities.listSubGroups({\n parentId: group.id!,\n first: 0,\n max: group.subGroupCount,\n briefRepresentation: true,\n realm,\n });\n const subGroupResults = await processGroupsRecursively(\n subgroups,\n entities,\n realm,\n );\n allGroups.push(...subGroupResults);\n }\n }\n\n return allGroups;\n}\n\nexport function* traverseGroups(\n group: GroupRepresentation,\n): IterableIterator<GroupRepresentationWithParent> {\n yield group;\n for (const g of group.subGroups ?? []) {\n (g as GroupRepresentationWithParent).parent = group.name!;\n yield* traverseGroups(g);\n }\n}\n\nexport const readKeycloakRealm = async (\n client: KeycloakAdminClient,\n config: KeycloakProviderConfig,\n logger: LoggerService,\n options?: {\n userQuerySize?: number;\n groupQuerySize?: number;\n userTransformer?: UserTransformer;\n groupTransformer?: GroupTransformer;\n },\n): Promise<{\n users: UserEntity[];\n groups: GroupEntity[];\n}> => {\n const kUsers = await getEntities(\n client.users,\n config,\n logger,\n options?.userQuerySize,\n );\n\n const topLevelKGroups = (await getEntities(\n client.groups,\n config,\n logger,\n options?.groupQuerySize,\n )) as GroupRepresentationWithParent[];\n\n let serverVersion: number;\n\n try {\n const serverInfo = await client.serverInfo.find();\n serverVersion = parseInt(\n serverInfo.systemInfo?.version?.slice(0, 2) || '',\n 10,\n );\n } catch (error) {\n throw new Error(`Failed to retrieve Keycloak server information: ${error}`);\n }\n\n const isVersion23orHigher = serverVersion >= 23;\n\n let rawKGroups: GroupRepresentationWithParent[] = [];\n\n if (isVersion23orHigher) {\n rawKGroups = await processGroupsRecursively(\n topLevelKGroups,\n client.groups as Groups,\n config.realm,\n );\n } else {\n rawKGroups = topLevelKGroups.reduce(\n (acc, g) => acc.concat(...traverseGroups(g)),\n [] as GroupRepresentationWithParent[],\n );\n }\n const kGroups = await Promise.all(\n rawKGroups.map(async g => {\n g.members = await getAllGroupMembers(\n client.groups as Groups,\n g.id!,\n config,\n options,\n );\n\n if (isVersion23orHigher) {\n if (g.subGroupCount! > 0) {\n g.subGroups = await client.groups.listSubGroups({\n parentId: g.id!,\n first: 0,\n max: g.subGroupCount,\n briefRepresentation: false,\n realm: config.realm,\n });\n }\n if (g.parentId) {\n const groupParent = await client.groups.findOne({\n id: g.parentId,\n realm: config.realm,\n });\n g.parent = groupParent?.name;\n }\n }\n\n return g;\n }),\n );\n\n const parsedGroups = await kGroups.reduce(async (promise, g) => {\n const partial = await promise;\n const entity = await parseGroup(g, config.realm, options?.groupTransformer);\n if (entity) {\n const group = {\n ...g,\n entity,\n } as GroupRepresentationWithParentAndEntity;\n partial.push(group);\n }\n return partial;\n }, Promise.resolve([] as GroupRepresentationWithParentAndEntity[]));\n\n const parsedUsers = await kUsers.reduce(async (promise, u) => {\n const partial = await promise;\n const entity = await parseUser(\n u,\n config.realm,\n parsedGroups,\n options?.userTransformer,\n );\n if (entity) {\n const user = { ...u, entity } as UserRepresentationWithEntity;\n partial.push(user);\n }\n return partial;\n }, Promise.resolve([] as UserRepresentationWithEntity[]));\n\n const groups = parsedGroups.map(g => {\n const entity = g.entity;\n entity.spec.members =\n g.entity.spec.members?.flatMap(m => {\n const name = parsedUsers.find(p => p.username === m)?.entity.metadata\n .name;\n return name ? [name] : [];\n }) ?? [];\n entity.spec.children =\n g.entity.spec.children?.flatMap(c => {\n const child = parsedGroups.find(p => p.name === c)?.entity.metadata\n .name;\n return child ? [child] : [];\n }) ?? [];\n entity.spec.parent = parsedGroups.find(\n p => p.name === entity.spec.parent,\n )?.entity.metadata.name;\n return entity;\n });\n\n return { users: parsedUsers.map(u => u.entity), groups };\n};\n"],"names":["noopGroupTransformer","KEYCLOAK_ID_ANNOTATION","KEYCLOAK_REALM_ANNOTATION","noopUserTransformer","KEYCLOAK_ENTITY_QUERY_SIZE"],"mappings":";;;;;AAwCO,MAAM,UAAa,GAAA,OACxB,aACA,EAAA,KAAA,EACA,gBACqC,KAAA;AACrC,EAAA,MAAM,cAAc,gBAAoB,IAAAA,iCAAA;AACxC,EAAA,MAAM,MAAsB,GAAA;AAAA,IAC1B,UAAY,EAAA,sBAAA;AAAA,IACZ,IAAM,EAAA,OAAA;AAAA,IACN,QAAU,EAAA;AAAA,MACR,MAAM,aAAc,CAAA,IAAA;AAAA,MACpB,WAAa,EAAA;AAAA,QACX,CAACC,gCAAsB,GAAG,aAAc,CAAA,EAAA;AAAA,QACxC,CAACC,mCAAyB,GAAG;AAAA;AAC/B,KACF;AAAA,IACA,IAAM,EAAA;AAAA,MACJ,IAAM,EAAA,OAAA;AAAA,MACN,OAAS,EAAA;AAAA,QACP,aAAa,aAAc,CAAA;AAAA,OAC7B;AAAA;AAAA,MAEA,QAAA,EAAU,cAAc,SAAW,EAAA,GAAA,CAAI,OAAK,CAAE,CAAA,IAAK,KAAK,EAAC;AAAA,MACzD,QAAQ,aAAc,CAAA,MAAA;AAAA,MACtB,SAAS,aAAc,CAAA;AAAA;AACzB,GACF;AAEA,EAAA,OAAO,MAAM,WAAA,CAAY,MAAQ,EAAA,aAAA,EAAe,KAAK,CAAA;AACvD;AAEO,MAAM,SAAY,GAAA,OACvB,IACA,EAAA,KAAA,EACA,gBAEA,eACoC,KAAA;AACpC,EAAA,MAAM,cAAc,eAAmB,IAAAC,gCAAA;AACvC,EAAA,MAAM,MAAqB,GAAA;AAAA,IACzB,UAAY,EAAA,sBAAA;AAAA,IACZ,IAAM,EAAA,MAAA;AAAA,IACN,QAAU,EAAA;AAAA,MACR,MAAM,IAAK,CAAA,QAAA;AAAA,MACX,WAAa,EAAA;AAAA,QACX,CAACF,gCAAsB,GAAG,IAAK,CAAA,EAAA;AAAA,QAC/B,CAACC,mCAAyB,GAAG;AAAA;AAC/B,KACF;AAAA,IACA,IAAM,EAAA;AAAA,MACJ,OAAS,EAAA;AAAA,QACP,OAAO,IAAK,CAAA,KAAA;AAAA,QACZ,GAAI,IAAA,CAAK,SAAa,IAAA,IAAA,CAAK,QACvB,GAAA;AAAA,UACE,WAAA,EAAa,CAAC,IAAA,CAAK,SAAW,EAAA,IAAA,CAAK,QAAQ,CAAA,CACxC,MAAO,CAAA,OAAO,CACd,CAAA,IAAA,CAAK,GAAG;AAAA,YAEb;AAAC,OACP;AAAA,MACA,UAAU,cACP,CAAA,MAAA,CAAO,CAAK,CAAA,KAAA,CAAA,CAAE,SAAS,QAAS,CAAA,IAAA,CAAK,QAAS,CAAC,EAC/C,GAAI,CAAA,CAAA,CAAA,KAAK,CAAE,CAAA,MAAA,CAAO,SAAS,IAAI;AAAA;AACpC,GACF;AAEA,EAAA,OAAO,MAAM,WAAA,CAAY,MAAQ,EAAA,IAAA,EAAM,OAAO,cAAc,CAAA;AAC9D;AAEA,eAAsB,WACpB,CAAA,QAAA,EACA,MACA,EAAA,MAAA,EACA,kBAA0BE,oCACe,EAAA;AACzC,EAAM,MAAA,cAAA,GAAiB,MAAM,QAAS,CAAA,KAAA,CAAM,EAAE,KAAO,EAAA,MAAA,CAAO,OAAO,CAAA;AACnE,EAAA,MAAM,WACJ,GAAA,OAAO,cAAmB,KAAA,QAAA,GAAW,iBAAiB,cAAe,CAAA,KAAA;AAEvE,EAAA,MAAM,SAAY,GAAA,IAAA,CAAK,IAAK,CAAA,WAAA,GAAc,eAAe,CAAA;AAGzD,EAAA,MAAM,iBAAiB,KAAM,CAAA,IAAA;AAAA,IAC3B,EAAE,QAAQ,SAAU,EAAA;AAAA,IACpB,CAAC,CAAA,EAAG,CACF,KAAA,QAAA,CACG,IAAK,CAAA;AAAA,MACJ,OAAO,MAAO,CAAA,KAAA;AAAA,MACd,GAAK,EAAA,eAAA;AAAA,MACL,OAAO,CAAI,GAAA;AAAA,KACZ,CACA,CAAA,KAAA;AAAA,MAAM,CACL,GAAA,KAAA,MAAA,CAAO,IAAK,CAAA,sCAAA,EAAwC,GAAG;AAAA;AACzD,GACN;AAEA,EAAA,MAAM,iBAAiB,MAAM,OAAA,CAAQ,GAAI,CAAA,cAAc,GAAG,IAAK,EAAA;AAI/D,EAAO,OAAA,aAAA;AACT;AAEA,eAAe,kBACb,CAAA,MAAA,EACA,OACA,EAAA,MAAA,EACA,OACmB,EAAA;AACnB,EAAM,MAAA,SAAA,GAAY,SAAS,aAAiB,IAAA,GAAA;AAE5C,EAAA,IAAI,aAAuB,EAAC;AAC5B,EAAA,IAAI,IAAO,GAAA,CAAA;AACX,EAAA,IAAI,YAAe,GAAA,CAAA;AAEnB,EAAG,GAAA;AACD,IAAM,MAAA,OAAA,GAAU,MAAM,MAAA,CAAO,WAAY,CAAA;AAAA,MACvC,EAAI,EAAA,OAAA;AAAA,MACJ,GAAK,EAAA,SAAA;AAAA,MACL,OAAO,MAAO,CAAA,KAAA;AAAA,MACd,OAAO,IAAO,GAAA;AAAA,KACf,CAAA;AAED,IAAI,IAAA,OAAA,CAAQ,SAAS,CAAG,EAAA;AACtB,MAAA,UAAA,GAAa,WAAW,MAAO,CAAA,OAAA,CAAQ,IAAI,CAAK,CAAA,KAAA,CAAA,CAAE,QAAS,CAAC,CAAA;AAC5D,MAAA,YAAA,GAAe,OAAQ,CAAA,MAAA;AAAA,KAClB,MAAA;AACL,MAAe,YAAA,GAAA,CAAA;AAAA;AAGjB,IAAA,IAAA,EAAA;AAAA,WACO,YAAe,GAAA,CAAA;AAExB,EAAO,OAAA,UAAA;AACT;AAEsB,eAAA,wBAAA,CACpB,cACA,EAAA,QAAA,EACA,KACA,EAAA;AACA,EAAA,MAAM,YAA6C,EAAC;AACpD,EAAA,KAAA,MAAW,SAAS,cAAgB,EAAA;AAClC,IAAA,SAAA,CAAU,KAAK,KAAK,CAAA;AAEpB,IAAI,IAAA,KAAA,CAAM,gBAAiB,CAAG,EAAA;AAC5B,MAAM,MAAA,SAAA,GAAY,MAAM,QAAA,CAAS,aAAc,CAAA;AAAA,QAC7C,UAAU,KAAM,CAAA,EAAA;AAAA,QAChB,KAAO,EAAA,CAAA;AAAA,QACP,KAAK,KAAM,CAAA,aAAA;AAAA,QACX,mBAAqB,EAAA,IAAA;AAAA,QACrB;AAAA,OACD,CAAA;AACD,MAAA,MAAM,kBAAkB,MAAM,wBAAA;AAAA,QAC5B,SAAA;AAAA,QACA,QAAA;AAAA,QACA;AAAA,OACF;AACA,MAAU,SAAA,CAAA,IAAA,CAAK,GAAG,eAAe,CAAA;AAAA;AACnC;AAGF,EAAO,OAAA,SAAA;AACT;AAEO,UAAU,eACf,KACiD,EAAA;AACjD,EAAM,MAAA,KAAA;AACN,EAAA,KAAA,MAAW,CAAK,IAAA,KAAA,CAAM,SAAa,IAAA,EAAI,EAAA;AACrC,IAAC,CAAA,CAAoC,SAAS,KAAM,CAAA,IAAA;AACpD,IAAA,OAAO,eAAe,CAAC,CAAA;AAAA;AAE3B;AAEO,MAAM,iBAAoB,GAAA,OAC/B,MACA,EAAA,MAAA,EACA,QACA,OASI,KAAA;AACJ,EAAA,MAAM,SAAS,MAAM,WAAA;AAAA,IACnB,MAAO,CAAA,KAAA;AAAA,IACP,MAAA;AAAA,IACA,MAAA;AAAA,IACA,OAAS,EAAA;AAAA,GACX;AAEA,EAAA,MAAM,kBAAmB,MAAM,WAAA;AAAA,IAC7B,MAAO,CAAA,MAAA;AAAA,IACP,MAAA;AAAA,IACA,MAAA;AAAA,IACA,OAAS,EAAA;AAAA,GACX;AAEA,EAAI,IAAA,aAAA;AAEJ,EAAI,IAAA;AACF,IAAA,MAAM,UAAa,GAAA,MAAM,MAAO,CAAA,UAAA,CAAW,IAAK,EAAA;AAChD,IAAgB,aAAA,GAAA,QAAA;AAAA,MACd,WAAW,UAAY,EAAA,OAAA,EAAS,KAAM,CAAA,CAAA,EAAG,CAAC,CAAK,IAAA,EAAA;AAAA,MAC/C;AAAA,KACF;AAAA,WACO,KAAO,EAAA;AACd,IAAA,MAAM,IAAI,KAAA,CAAM,CAAmD,gDAAA,EAAA,KAAK,CAAE,CAAA,CAAA;AAAA;AAG5E,EAAA,MAAM,sBAAsB,aAAiB,IAAA,EAAA;AAE7C,EAAA,IAAI,aAA8C,EAAC;AAEnD,EAAA,IAAI,mBAAqB,EAAA;AACvB,IAAA,UAAA,GAAa,MAAM,wBAAA;AAAA,MACjB,eAAA;AAAA,MACA,MAAO,CAAA,MAAA;AAAA,MACP,MAAO,CAAA;AAAA,KACT;AAAA,GACK,MAAA;AACL,IAAA,UAAA,GAAa,eAAgB,CAAA,MAAA;AAAA,MAC3B,CAAC,KAAK,CAAM,KAAA,GAAA,CAAI,OAAO,GAAG,cAAA,CAAe,CAAC,CAAC,CAAA;AAAA,MAC3C;AAAC,KACH;AAAA;AAEF,EAAM,MAAA,OAAA,GAAU,MAAM,OAAQ,CAAA,GAAA;AAAA,IAC5B,UAAA,CAAW,GAAI,CAAA,OAAM,CAAK,KAAA;AACxB,MAAA,CAAA,CAAE,UAAU,MAAM,kBAAA;AAAA,QAChB,MAAO,CAAA,MAAA;AAAA,QACP,CAAE,CAAA,EAAA;AAAA,QACF,MAAA;AAAA,QACA;AAAA,OACF;AAEA,MAAA,IAAI,mBAAqB,EAAA;AACvB,QAAI,IAAA,CAAA,CAAE,gBAAiB,CAAG,EAAA;AACxB,UAAA,CAAA,CAAE,SAAY,GAAA,MAAM,MAAO,CAAA,MAAA,CAAO,aAAc,CAAA;AAAA,YAC9C,UAAU,CAAE,CAAA,EAAA;AAAA,YACZ,KAAO,EAAA,CAAA;AAAA,YACP,KAAK,CAAE,CAAA,aAAA;AAAA,YACP,mBAAqB,EAAA,KAAA;AAAA,YACrB,OAAO,MAAO,CAAA;AAAA,WACf,CAAA;AAAA;AAEH,QAAA,IAAI,EAAE,QAAU,EAAA;AACd,UAAA,MAAM,WAAc,GAAA,MAAM,MAAO,CAAA,MAAA,CAAO,OAAQ,CAAA;AAAA,YAC9C,IAAI,CAAE,CAAA,QAAA;AAAA,YACN,OAAO,MAAO,CAAA;AAAA,WACf,CAAA;AACD,UAAA,CAAA,CAAE,SAAS,WAAa,EAAA,IAAA;AAAA;AAC1B;AAGF,MAAO,OAAA,CAAA;AAAA,KACR;AAAA,GACH;AAEA,EAAA,MAAM,eAAe,MAAM,OAAA,CAAQ,MAAO,CAAA,OAAO,SAAS,CAAM,KAAA;AAC9D,IAAA,MAAM,UAAU,MAAM,OAAA;AACtB,IAAA,MAAM,SAAS,MAAM,UAAA,CAAW,GAAG,MAAO,CAAA,KAAA,EAAO,SAAS,gBAAgB,CAAA;AAC1E,IAAA,IAAI,MAAQ,EAAA;AACV,MAAA,MAAM,KAAQ,GAAA;AAAA,QACZ,GAAG,CAAA;AAAA,QACH;AAAA,OACF;AACA,MAAA,OAAA,CAAQ,KAAK,KAAK,CAAA;AAAA;AAEpB,IAAO,OAAA,OAAA;AAAA,GACN,EAAA,OAAA,CAAQ,OAAQ,CAAA,EAA8C,CAAC,CAAA;AAElE,EAAA,MAAM,cAAc,MAAM,MAAA,CAAO,MAAO,CAAA,OAAO,SAAS,CAAM,KAAA;AAC5D,IAAA,MAAM,UAAU,MAAM,OAAA;AACtB,IAAA,MAAM,SAAS,MAAM,SAAA;AAAA,MACnB,CAAA;AAAA,MACA,MAAO,CAAA,KAAA;AAAA,MACP,YAAA;AAAA,MACA,OAAS,EAAA;AAAA,KACX;AACA,IAAA,IAAI,MAAQ,EAAA;AACV,MAAA,MAAM,IAAO,GAAA,EAAE,GAAG,CAAA,EAAG,MAAO,EAAA;AAC5B,MAAA,OAAA,CAAQ,KAAK,IAAI,CAAA;AAAA;AAEnB,IAAO,OAAA,OAAA;AAAA,GACN,EAAA,OAAA,CAAQ,OAAQ,CAAA,EAAoC,CAAC,CAAA;AAExD,EAAM,MAAA,MAAA,GAAS,YAAa,CAAA,GAAA,CAAI,CAAK,CAAA,KAAA;AACnC,IAAA,MAAM,SAAS,CAAE,CAAA,MAAA;AACjB,IAAA,MAAA,CAAO,KAAK,OACV,GAAA,CAAA,CAAE,OAAO,IAAK,CAAA,OAAA,EAAS,QAAQ,CAAK,CAAA,KAAA;AAClC,MAAM,MAAA,IAAA,GAAO,YAAY,IAAK,CAAA,CAAA,CAAA,KAAK,EAAE,QAAa,KAAA,CAAC,CAAG,EAAA,MAAA,CAAO,QAC1D,CAAA,IAAA;AACH,MAAA,OAAO,IAAO,GAAA,CAAC,IAAI,CAAA,GAAI,EAAC;AAAA,KACzB,KAAK,EAAC;AACT,IAAA,MAAA,CAAO,KAAK,QACV,GAAA,CAAA,CAAE,OAAO,IAAK,CAAA,QAAA,EAAU,QAAQ,CAAK,CAAA,KAAA;AACnC,MAAM,MAAA,KAAA,GAAQ,aAAa,IAAK,CAAA,CAAA,CAAA,KAAK,EAAE,IAAS,KAAA,CAAC,CAAG,EAAA,MAAA,CAAO,QACxD,CAAA,IAAA;AACH,MAAA,OAAO,KAAQ,GAAA,CAAC,KAAK,CAAA,GAAI,EAAC;AAAA,KAC3B,KAAK,EAAC;AACT,IAAO,MAAA,CAAA,IAAA,CAAK,SAAS,YAAa,CAAA,IAAA;AAAA,MAChC,CAAK,CAAA,KAAA,CAAA,CAAE,IAAS,KAAA,MAAA,CAAO,IAAK,CAAA;AAAA,KAC9B,EAAG,OAAO,QAAS,CAAA,IAAA;AACnB,IAAO,OAAA,MAAA;AAAA,GACR,CAAA;AAED,EAAO,OAAA,EAAE,OAAO,WAAY,CAAA,GAAA,CAAI,OAAK,CAAE,CAAA,MAAM,GAAG,MAAO,EAAA;AACzD;;;;;;;;;"}
1
+ {"version":3,"file":"read.cjs.js","sources":["../../src/lib/read.ts"],"sourcesContent":["/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { LoggerService } from '@backstage/backend-plugin-api';\nimport type { GroupEntity, UserEntity } from '@backstage/catalog-model';\n\nimport type KeycloakAdminClient from '@keycloak/keycloak-admin-client';\nimport type GroupRepresentation from '@keycloak/keycloak-admin-client/lib/defs/groupRepresentation';\nimport type UserRepresentation from '@keycloak/keycloak-admin-client/lib/defs/userRepresentation';\nimport type { Groups } from '@keycloak/keycloak-admin-client/lib/resources/groups';\nimport type { Users } from '@keycloak/keycloak-admin-client/lib/resources/users';\nimport { LimitFunction } from 'p-limit';\n\nimport { KeycloakProviderConfig } from './config';\nimport {\n KEYCLOAK_ENTITY_QUERY_SIZE,\n KEYCLOAK_ID_ANNOTATION,\n KEYCLOAK_REALM_ANNOTATION,\n} from './constants';\nimport { noopGroupTransformer, noopUserTransformer } from './transformers';\nimport {\n GroupRepresentationWithParent,\n GroupRepresentationWithParentAndEntity,\n GroupTransformer,\n UserRepresentationWithEntity,\n UserTransformer,\n} from './types';\nimport { ensureTokenValid } from './authenticate';\n\nexport const parseGroup = async (\n keycloakGroup: GroupRepresentationWithParent,\n realm: string,\n groupTransformer?: GroupTransformer,\n): Promise<GroupEntity | undefined> => {\n const transformer = groupTransformer ?? noopGroupTransformer;\n const entity: GroupEntity = {\n apiVersion: 'backstage.io/v1beta1',\n kind: 'Group',\n metadata: {\n name: keycloakGroup.name!,\n annotations: {\n [KEYCLOAK_ID_ANNOTATION]: keycloakGroup.id!,\n [KEYCLOAK_REALM_ANNOTATION]: realm,\n },\n },\n spec: {\n type: 'group',\n profile: {\n displayName: keycloakGroup.name!,\n },\n // children, parent and members are updated again after all group and user transformers applied.\n children: keycloakGroup.subGroups?.map(g => g.name!) ?? [],\n parent: keycloakGroup.parent,\n members: keycloakGroup.members,\n },\n };\n\n return await transformer(entity, keycloakGroup, realm);\n};\n\nexport const parseUser = async (\n user: UserRepresentation,\n realm: string,\n keycloakGroups: GroupRepresentationWithParentAndEntity[],\n groupIndex: Map<string, string[]>,\n userTransformer?: UserTransformer,\n): Promise<UserEntity | undefined> => {\n const transformer = userTransformer ?? noopUserTransformer;\n const entity: UserEntity = {\n apiVersion: 'backstage.io/v1beta1',\n kind: 'User',\n metadata: {\n name: user.username!,\n annotations: {\n [KEYCLOAK_ID_ANNOTATION]: user.id!,\n [KEYCLOAK_REALM_ANNOTATION]: realm,\n },\n },\n spec: {\n profile: {\n email: user.email,\n ...(user.firstName || user.lastName\n ? {\n displayName: [user.firstName, user.lastName]\n .filter(Boolean)\n .join(' '),\n }\n : {}),\n },\n memberOf: groupIndex.get(user.username!) ?? [],\n },\n };\n\n return await transformer(entity, user, realm, keycloakGroups);\n};\n\nexport async function getEntities<T extends Users | Groups>(\n getEntitiesFn: () => Promise<T>,\n config: KeycloakProviderConfig,\n logger: LoggerService,\n limit: LimitFunction,\n entityQuerySize: number = KEYCLOAK_ENTITY_QUERY_SIZE,\n): Promise<Awaited<ReturnType<T['find']>>> {\n const entitiesAPI = await getEntitiesFn();\n const rawEntityCount = await entitiesAPI.count({ realm: config.realm });\n const entityCount =\n typeof rawEntityCount === 'number' ? rawEntityCount : rawEntityCount.count;\n\n const pageCount = Math.ceil(entityCount / entityQuerySize);\n\n // The next line acts like range in python\n const entityPromises = Array.from({ length: pageCount }, (_, i) =>\n limit(() =>\n getEntitiesFn().then(entities => {\n return entities\n .find({\n realm: config.realm,\n max: entityQuerySize,\n first: i * entityQuerySize,\n })\n .then(ents => {\n logger.debug(\n `Importing keycloak entities batch with index ${i} from pages: ${pageCount}`,\n );\n return ents;\n })\n .catch(err => {\n logger.warn('Failed to retieve Keycloak entities.', err);\n return [];\n }) as ReturnType<T['find']>;\n }),\n ),\n );\n\n const entityResults = (await Promise.all(entityPromises)).flat() as Awaited<\n ReturnType<T['find']>\n >;\n\n return entityResults;\n}\n\nasync function getAllGroupMembers<T extends Groups>(\n groupsAPI: () => Promise<T>,\n groupId: string,\n config: KeycloakProviderConfig,\n options?: { userQuerySize?: number },\n): Promise<string[]> {\n const querySize = options?.userQuerySize || 100;\n\n let allMembers: string[] = [];\n let page = 0;\n let totalMembers = 0;\n\n do {\n const groups = await groupsAPI();\n const members = await groups.listMembers({\n id: groupId,\n max: querySize,\n realm: config.realm,\n first: page * querySize,\n });\n\n if (members.length > 0) {\n allMembers = allMembers.concat(members.map(m => m.username!));\n totalMembers = members.length; // Get the number of members retrieved\n } else {\n totalMembers = 0; // No members retrieved\n }\n\n page++;\n } while (totalMembers > 0);\n\n return allMembers;\n}\n\nexport async function processGroupsRecursively(\n kcAdminClient: KeycloakAdminClient,\n config: KeycloakProviderConfig,\n logger: LoggerService,\n topLevelGroups: GroupRepresentationWithParent[],\n) {\n const allGroups: GroupRepresentationWithParent[] = [];\n for (const group of topLevelGroups) {\n allGroups.push(group);\n\n if (group.subGroupCount! > 0) {\n await ensureTokenValid(kcAdminClient, config, logger);\n const subgroups = await kcAdminClient.groups.listSubGroups({\n parentId: group.id!,\n first: 0,\n max: group.subGroupCount,\n briefRepresentation: true,\n });\n const subGroupResults = await processGroupsRecursively(\n kcAdminClient,\n config,\n logger,\n subgroups,\n );\n allGroups.push(...subGroupResults);\n }\n }\n\n return allGroups;\n}\n\nexport function* traverseGroups(\n group: GroupRepresentation,\n): IterableIterator<GroupRepresentationWithParent> {\n yield group;\n for (const g of group.subGroups ?? []) {\n (g as GroupRepresentationWithParent).parent = group.name!;\n yield* traverseGroups(g);\n }\n}\n\nexport const readKeycloakRealm = async (\n client: KeycloakAdminClient,\n config: KeycloakProviderConfig,\n logger: LoggerService,\n limit: LimitFunction,\n options?: {\n userQuerySize?: number;\n groupQuerySize?: number;\n userTransformer?: UserTransformer;\n groupTransformer?: GroupTransformer;\n },\n): Promise<{\n users: UserEntity[];\n groups: GroupEntity[];\n}> => {\n const kUsers = await getEntities(\n async () => {\n await ensureTokenValid(client, config, logger);\n return client.users;\n },\n config,\n logger,\n limit,\n options?.userQuerySize,\n );\n logger.debug(`Fetched ${kUsers.length} users from Keycloak`);\n\n const topLevelKGroups = (await getEntities(\n async () => {\n await ensureTokenValid(client, config, logger);\n return client.groups;\n },\n config,\n logger,\n limit,\n options?.groupQuerySize,\n )) as GroupRepresentationWithParent[];\n logger.debug(`Fetched ${topLevelKGroups.length} groups from Keycloak`);\n\n let serverVersion: number;\n\n try {\n await ensureTokenValid(client, config, logger);\n const serverInfo = await client.serverInfo.find();\n serverVersion = parseInt(\n serverInfo.systemInfo?.version?.slice(0, 2) || '',\n 10,\n );\n } catch (error) {\n throw new Error(`Failed to retrieve Keycloak server information: ${error}`);\n }\n\n const isVersion23orHigher = serverVersion >= 23;\n\n let rawKGroups: GroupRepresentationWithParent[] = [];\n\n logger.debug(`Processing groups recursively`);\n if (isVersion23orHigher) {\n rawKGroups = await processGroupsRecursively(\n client,\n config,\n logger,\n topLevelKGroups,\n );\n } else {\n rawKGroups = topLevelKGroups.reduce(\n (acc, g) => acc.concat(...traverseGroups(g)),\n [] as GroupRepresentationWithParent[],\n );\n }\n\n logger.debug(`Fetching group members for keycloak groups and list subgroups`);\n const kGroups = await Promise.all(\n rawKGroups.map(g =>\n limit(async () => {\n g.members = await getAllGroupMembers(\n async () => {\n await ensureTokenValid(client, config, logger);\n return client.groups as Groups;\n },\n g.id!,\n config,\n options,\n );\n\n if (isVersion23orHigher) {\n if (g.subGroupCount! > 0) {\n await ensureTokenValid(client, config, logger);\n g.subGroups = await client.groups.listSubGroups({\n parentId: g.id!,\n first: 0,\n max: g.subGroupCount,\n briefRepresentation: false,\n realm: config.realm,\n });\n }\n if (g.parentId) {\n await ensureTokenValid(client, config, logger);\n const groupParent = await client.groups.findOne({\n id: g.parentId,\n realm: config.realm,\n });\n g.parent = groupParent?.name;\n }\n }\n\n return g;\n }),\n ),\n );\n\n logger.debug(`Parsing groups`);\n const parsedGroups = await Promise.all(\n kGroups.map(async g => {\n // it is possible if fetch request failed\n if (!g) {\n return null;\n }\n const entity = await parseGroup(\n g,\n config.realm,\n options?.groupTransformer,\n );\n if (entity) {\n return { ...g, entity } as GroupRepresentationWithParentAndEntity;\n }\n return null;\n }),\n );\n const filteredParsedGroups = parsedGroups.filter(\n (group): group is GroupRepresentationWithParentAndEntity => group !== null,\n );\n\n const groupIndex = new Map<string, string[]>();\n filteredParsedGroups.forEach(group => {\n if (group.members) {\n group.members.forEach(member => {\n if (!groupIndex.has(member)) {\n groupIndex.set(member, []);\n }\n groupIndex.get(member)?.push(group.entity.metadata.name);\n });\n }\n });\n\n logger.debug('Parsing users');\n const parsedUsers = await Promise.all(\n kUsers.map(async u => {\n // it is possible if fetch request failed\n if (!u) {\n return null;\n }\n const entity = await parseUser(\n u,\n config.realm,\n filteredParsedGroups,\n groupIndex,\n options?.userTransformer,\n );\n if (entity) {\n return { ...u, entity } as UserRepresentationWithEntity;\n }\n return null;\n }),\n );\n const filteredParsedUsers = parsedUsers.filter(\n (user): user is UserRepresentationWithEntity => user !== null,\n );\n\n logger.debug(`Set up group members and children information`);\n\n const userMap = new Map(\n filteredParsedUsers.map(user => [user.username, user.entity.metadata.name]),\n );\n\n const groupMap = new Map(\n filteredParsedGroups.map(group => [group.name, group.entity.metadata.name]),\n );\n\n const groups = filteredParsedGroups.map(g => {\n const entity = g.entity;\n entity.spec.members =\n g.entity.spec.members?.flatMap(m => userMap.get(m) ?? []) ?? [];\n entity.spec.children =\n g.entity.spec.children?.flatMap(c => groupMap.get(c) ?? []) ?? [];\n entity.spec.parent = groupMap.get(entity.spec.parent);\n return entity;\n });\n\n logger.info(\n `Prepared to ingest ${parsedUsers.length} users and ${groups.length} groups into the catalog from Keycloak`,\n );\n\n return { users: filteredParsedUsers.map(u => u.entity), groups };\n};\n"],"names":["noopGroupTransformer","KEYCLOAK_ID_ANNOTATION","KEYCLOAK_REALM_ANNOTATION","noopUserTransformer","KEYCLOAK_ENTITY_QUERY_SIZE","ensureTokenValid"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"transformers.cjs.js","sources":["../../src/lib/transformers.ts"],"sourcesContent":["/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport type { GroupTransformer, UserTransformer } from './types';\n\n/**\n * @public\n */\nexport const noopGroupTransformer: GroupTransformer = async (\n entity,\n _user,\n _realm,\n) => entity;\n\n/**\n * @public\n */\nexport const noopUserTransformer: UserTransformer = async (\n entity,\n _user,\n _realm,\n _groups,\n) => entity;\n\n/**\n * @public\n * User transformer that sanitizes .metadata.name from email address to a valid name\n */\nexport const sanitizeEmailTransformer: UserTransformer = async (\n entity,\n _user,\n _realm,\n _groups,\n) => {\n entity.metadata.name = entity.metadata.name.replace(/[^a-zA-Z0-9]/g, '-');\n return entity;\n};\n"],"names":[],"mappings":";;AAoBO,MAAM,oBAAyC,GAAA,OACpD,MACA,EAAA,KAAA,EACA,MACG,KAAA;AAKE,MAAM,mBAAuC,GAAA,OAClD,MACA,EAAA,KAAA,EACA,QACA,OACG,KAAA;AAME,MAAM,wBAA4C,GAAA,OACvD,MACA,EAAA,KAAA,EACA,QACA,OACG,KAAA;AACH,EAAA,MAAA,CAAO,SAAS,IAAO,GAAA,MAAA,CAAO,SAAS,IAAK,CAAA,OAAA,CAAQ,iBAAiB,GAAG,CAAA;AACxE,EAAO,OAAA,MAAA;AACT;;;;;;"}
1
+ {"version":3,"file":"transformers.cjs.js","sources":["../../src/lib/transformers.ts"],"sourcesContent":["/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport type { GroupTransformer, UserTransformer } from './types';\n\n/**\n * @public\n * Group transformer that does nothing.\n */\nexport const noopGroupTransformer: GroupTransformer = async (\n entity,\n _user,\n _realm,\n) => entity;\n\n/**\n * @public\n * User transformer that does nothing.\n */\nexport const noopUserTransformer: UserTransformer = async (\n entity,\n _user,\n _realm,\n _groups,\n) => entity;\n\n/**\n * @public\n * User transformer that sanitizes .metadata.name from email address to a valid name\n */\nexport const sanitizeEmailTransformer: UserTransformer = async (\n entity,\n _user,\n _realm,\n _groups,\n) => {\n entity.metadata.name = entity.metadata.name.replace(/[^a-zA-Z0-9]/g, '-');\n return entity;\n};\n"],"names":[],"mappings":";;AAqBO,MAAM,oBAAyC,GAAA,OACpD,MACA,EAAA,KAAA,EACA,MACG,KAAA;AAME,MAAM,mBAAuC,GAAA,OAClD,MACA,EAAA,KAAA,EACA,QACA,OACG,KAAA;AAME,MAAM,wBAA4C,GAAA,OACvD,MACA,EAAA,KAAA,EACA,QACA,OACG,KAAA;AACH,EAAA,MAAA,CAAO,SAAS,IAAO,GAAA,MAAA,CAAO,SAAS,IAAK,CAAA,OAAA,CAAQ,iBAAiB,GAAG,CAAA;AACxE,EAAO,OAAA,MAAA;AACT;;;;;;"}
@@ -8,6 +8,7 @@ var uuid = require('uuid');
8
8
  var constants = require('../lib/constants.cjs.js');
9
9
  var config = require('../lib/config.cjs.js');
10
10
  var read = require('../lib/read.cjs.js');
11
+ var authenticate = require('../lib/authenticate.cjs.js');
11
12
 
12
13
  function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
13
14
 
@@ -54,6 +55,12 @@ class KeycloakOrgEntityProvider {
54
55
  }
55
56
  connection;
56
57
  scheduleFn;
58
+ /**
59
+ * Static builder method to create multiple KeycloakOrgEntityProvider instances from a single config.
60
+ * @param deps - The dependencies required for the provider, including the configuration and logger.
61
+ * @param options - Options for scheduling tasks and transforming users and groups.
62
+ * @returns An array of KeycloakOrgEntityProvider instances.
63
+ */
57
64
  static fromConfig(deps, options) {
58
65
  const { config: config$1, logger } = deps;
59
66
  return config.readProviderConfigs(config$1).map((providerConfig) => {
@@ -80,9 +87,16 @@ class KeycloakOrgEntityProvider {
80
87
  return provider;
81
88
  });
82
89
  }
90
+ /**
91
+ * Returns the name of this entity provider.
92
+ */
83
93
  getProviderName() {
84
94
  return `KeycloakOrgEntityProvider:${this.options.id}`;
85
95
  }
96
+ /**
97
+ * Connect to Backstage catalog entity provider
98
+ * @param connection - The connection to the catalog API ingestor, which allows the provision of new entities.
99
+ */
86
100
  async connect(connection) {
87
101
  this.connection = connection;
88
102
  await this.scheduleFn?.();
@@ -106,30 +120,16 @@ class KeycloakOrgEntityProvider {
106
120
  baseUrl: provider.baseUrl,
107
121
  realmName: provider.loginRealm
108
122
  });
109
- let credentials;
110
- if (provider.username && provider.password) {
111
- credentials = {
112
- grantType: "password",
113
- clientId: provider.clientId ?? "admin-cli",
114
- username: provider.username,
115
- password: provider.password
116
- };
117
- } else if (provider.clientId && provider.clientSecret) {
118
- credentials = {
119
- grantType: "client_credentials",
120
- clientId: provider.clientId,
121
- clientSecret: provider.clientSecret
122
- };
123
- } else {
124
- throw new errors.InputError(
125
- `username and password or clientId and clientSecret must be provided.`
126
- );
127
- }
128
- await kcAdminClient.auth(credentials);
123
+ await authenticate.authenticate(kcAdminClient, provider, logger);
124
+ const pLimitCJSModule = await inclusion__default.default("p-limit");
125
+ const limitFunc = pLimitCJSModule.default;
126
+ const concurrency = provider.maxConcurrency ?? 20;
127
+ const limit = limitFunc(concurrency);
129
128
  const { users, groups } = await read.readKeycloakRealm(
130
129
  kcAdminClient,
131
130
  provider,
132
131
  logger,
132
+ limit,
133
133
  {
134
134
  userQuerySize: provider.userQuerySize,
135
135
  groupQuerySize: provider.groupQuerySize,
@@ -147,6 +147,10 @@ class KeycloakOrgEntityProvider {
147
147
  });
148
148
  markCommitComplete();
149
149
  }
150
+ /**
151
+ * Periodically schedules a task to read Keycloak user and group information, parse it, and provision it to the Backstage catalog.
152
+ * @param taskRunner - The task runner to use for scheduling tasks.
153
+ */
150
154
  schedule(taskRunner) {
151
155
  this.scheduleFn = async () => {
152
156
  const id = `${this.getProviderName()}:refresh`;
@@ -1 +1 @@
1
- {"version":3,"file":"KeycloakOrgEntityProvider.cjs.js","sources":["../../src/providers/KeycloakOrgEntityProvider.ts"],"sourcesContent":["/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type {\n LoggerService,\n SchedulerService,\n SchedulerServiceTaskRunner,\n} from '@backstage/backend-plugin-api';\nimport {\n ANNOTATION_LOCATION,\n ANNOTATION_ORIGIN_LOCATION,\n type Entity,\n} from '@backstage/catalog-model';\nimport type { Config } from '@backstage/config';\nimport { InputError, isError, NotFoundError } from '@backstage/errors';\nimport type {\n EntityProvider,\n EntityProviderConnection,\n} from '@backstage/plugin-catalog-node';\n\nimport type { Credentials } from '@keycloak/keycloak-admin-client/lib/utils/auth';\n// @ts-ignore\nimport inclusion from 'inclusion';\nimport { merge } from 'lodash';\nimport * as uuid from 'uuid';\n\nimport {\n GroupTransformer,\n KEYCLOAK_ID_ANNOTATION,\n KeycloakProviderConfig,\n UserTransformer,\n} from '../lib';\nimport { readProviderConfigs } from '../lib/config';\nimport { readKeycloakRealm } from '../lib/read';\n\n/**\n * Options for {@link KeycloakOrgEntityProvider}.\n *\n * @public\n */\nexport interface KeycloakOrgEntityProviderOptions {\n /**\n * A unique, stable identifier for this provider.\n *\n * @example \"production\"\n */\n id: string;\n\n /**\n * The refresh schedule to use.\n * @remarks\n *\n * You can pass in the result of\n * {@link @backstage/backend-plugin-api#SchedulerService.createScheduledTaskRunner}\n * to enable automatic scheduling of tasks.\n */\n schedule?: SchedulerServiceTaskRunner;\n\n /**\n * Scheduler used to schedule refreshes based on\n * the schedule config.\n */\n scheduler?: SchedulerService;\n\n /**\n * The logger to use.\n */\n logger: LoggerService;\n\n /**\n * The function that transforms a user entry in LDAP to an entity.\n */\n userTransformer?: UserTransformer;\n\n /**\n * The function that transforms a group entry in LDAP to an entity.\n */\n groupTransformer?: GroupTransformer;\n}\n\n// Makes sure that emitted entities have a proper location\nexport const withLocations = (\n baseUrl: string,\n realm: string,\n entity: Entity,\n): Entity => {\n const kind = entity.kind === 'Group' ? 'groups' : 'users';\n const location = `url:${baseUrl}/admin/realms/${realm}/${kind}/${entity.metadata.annotations?.[KEYCLOAK_ID_ANNOTATION]}`;\n return merge(\n {\n metadata: {\n annotations: {\n [ANNOTATION_LOCATION]: location,\n [ANNOTATION_ORIGIN_LOCATION]: location,\n },\n },\n },\n entity,\n ) as Entity;\n};\n\n/**\n * Ingests org data (users and groups) from GitHub.\n *\n * @public\n */\nexport class KeycloakOrgEntityProvider implements EntityProvider {\n private connection?: EntityProviderConnection;\n private scheduleFn?: () => Promise<void>;\n\n static fromConfig(\n deps: {\n config: Config;\n logger: LoggerService;\n },\n options: (\n | { schedule: SchedulerServiceTaskRunner }\n | { scheduler: SchedulerService }\n ) & {\n userTransformer?: UserTransformer;\n groupTransformer?: GroupTransformer;\n },\n ): KeycloakOrgEntityProvider[] {\n const { config, logger } = deps;\n return readProviderConfigs(config).map(providerConfig => {\n let taskRunner: SchedulerServiceTaskRunner | string;\n if ('scheduler' in options && providerConfig.schedule) {\n // Create a scheduled task runner using the provided scheduler and schedule configuration\n taskRunner = options.scheduler.createScheduledTaskRunner(\n providerConfig.schedule,\n );\n } else if ('schedule' in options) {\n // Use the provided schedule directly\n taskRunner = options.schedule;\n } else {\n throw new InputError(\n `No schedule provided via config for KeycloakOrgEntityProvider:${providerConfig.id}.`,\n );\n }\n\n const provider = new KeycloakOrgEntityProvider({\n id: providerConfig.id,\n provider: providerConfig,\n logger: logger,\n taskRunner: taskRunner,\n userTransformer: options.userTransformer,\n groupTransformer: options.groupTransformer,\n });\n\n return provider;\n });\n }\n\n constructor(\n private options: {\n id: string;\n provider: KeycloakProviderConfig;\n logger: LoggerService;\n taskRunner: SchedulerServiceTaskRunner;\n userTransformer?: UserTransformer;\n groupTransformer?: GroupTransformer;\n },\n ) {\n this.schedule(options.taskRunner);\n }\n\n getProviderName(): string {\n return `KeycloakOrgEntityProvider:${this.options.id}`;\n }\n\n async connect(connection: EntityProviderConnection) {\n this.connection = connection;\n await this.scheduleFn?.();\n }\n\n /**\n * Runs one complete ingestion loop. Call this method regularly at some\n * appropriate cadence.\n */\n async read(options?: { logger?: LoggerService }) {\n if (!this.connection) {\n throw new NotFoundError('Not initialized');\n }\n\n const logger = options?.logger ?? this.options.logger;\n const provider = this.options.provider;\n\n const { markReadComplete } = trackProgress(logger);\n const KeyCloakAdminClientModule = await inclusion(\n '@keycloak/keycloak-admin-client',\n );\n const KeyCloakAdminClient = KeyCloakAdminClientModule.default;\n\n const kcAdminClient = new KeyCloakAdminClient({\n baseUrl: provider.baseUrl,\n realmName: provider.loginRealm,\n });\n\n let credentials: Credentials;\n\n if (provider.username && provider.password) {\n credentials = {\n grantType: 'password',\n clientId: provider.clientId ?? 'admin-cli',\n username: provider.username,\n password: provider.password,\n };\n } else if (provider.clientId && provider.clientSecret) {\n credentials = {\n grantType: 'client_credentials',\n clientId: provider.clientId,\n clientSecret: provider.clientSecret,\n };\n } else {\n throw new InputError(\n `username and password or clientId and clientSecret must be provided.`,\n );\n }\n\n await kcAdminClient.auth(credentials);\n\n const { users, groups } = await readKeycloakRealm(\n kcAdminClient,\n provider,\n logger,\n {\n userQuerySize: provider.userQuerySize,\n groupQuerySize: provider.groupQuerySize,\n userTransformer: this.options.userTransformer,\n groupTransformer: this.options.groupTransformer,\n },\n );\n\n const { markCommitComplete } = markReadComplete({ users, groups });\n\n await this.connection.applyMutation({\n type: 'full',\n entities: [...users, ...groups].map(entity => ({\n locationKey: `keycloak-org-provider:${this.options.id}`,\n entity: withLocations(provider.baseUrl, provider.realm, entity),\n })),\n });\n\n markCommitComplete();\n }\n\n schedule(taskRunner: SchedulerServiceTaskRunner) {\n this.scheduleFn = async () => {\n const id = `${this.getProviderName()}:refresh`;\n await taskRunner.run({\n id,\n fn: async () => {\n const logger = this.options.logger.child({\n class: KeycloakOrgEntityProvider.prototype.constructor.name,\n taskId: id,\n taskInstanceId: uuid.v4(),\n });\n\n try {\n await this.read({ logger });\n } catch (error) {\n if (isError(error)) {\n // Ensure that we don't log any sensitive internal data:\n logger.error('Error while syncing Keycloak users and groups', {\n // Default Error properties:\n name: error.name,\n cause: error.cause,\n message: error.message,\n stack: error.stack,\n // Additional status code if available:\n status: (error.response as { status?: string })?.status,\n });\n }\n }\n },\n });\n };\n }\n}\n\n// Helps wrap the timing and logging behaviors\nfunction trackProgress(logger: LoggerService) {\n let timestamp = Date.now();\n let summary: string;\n\n logger.info('Reading Keycloak users and groups');\n\n function markReadComplete(read: { users: unknown[]; groups: unknown[] }) {\n summary = `${read.users.length} Keycloak users and ${read.groups.length} Keycloak groups`;\n const readDuration = ((Date.now() - timestamp) / 1000).toFixed(1);\n timestamp = Date.now();\n logger.info(`Read ${summary} in ${readDuration} seconds. Committing...`);\n return { markCommitComplete };\n }\n\n function markCommitComplete() {\n const commitDuration = ((Date.now() - timestamp) / 1000).toFixed(1);\n logger.info(`Committed ${summary} in ${commitDuration} seconds.`);\n }\n\n return { markReadComplete };\n}\n"],"names":["KEYCLOAK_ID_ANNOTATION","merge","ANNOTATION_LOCATION","ANNOTATION_ORIGIN_LOCATION","config","readProviderConfigs","InputError","NotFoundError","inclusion","readKeycloakRealm","uuid","isError"],"mappings":"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"}
1
+ {"version":3,"file":"KeycloakOrgEntityProvider.cjs.js","sources":["../../src/providers/KeycloakOrgEntityProvider.ts"],"sourcesContent":["/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type {\n LoggerService,\n SchedulerService,\n SchedulerServiceTaskRunner,\n} from '@backstage/backend-plugin-api';\nimport {\n ANNOTATION_LOCATION,\n ANNOTATION_ORIGIN_LOCATION,\n type Entity,\n} from '@backstage/catalog-model';\nimport type { Config } from '@backstage/config';\nimport { InputError, isError, NotFoundError } from '@backstage/errors';\nimport type {\n EntityProvider,\n EntityProviderConnection,\n} from '@backstage/plugin-catalog-node';\n\n// @ts-ignore\nimport inclusion from 'inclusion';\nimport { merge } from 'lodash';\nimport { LimitFunction } from 'p-limit';\nimport * as uuid from 'uuid';\n\nimport {\n GroupTransformer,\n KEYCLOAK_ID_ANNOTATION,\n KeycloakProviderConfig,\n UserTransformer,\n} from '../lib';\nimport { readProviderConfigs } from '../lib/config';\nimport { readKeycloakRealm } from '../lib/read';\nimport { authenticate } from '../lib/authenticate';\n\n/**\n * Options for {@link KeycloakOrgEntityProvider}.\n *\n * @public\n */\nexport interface KeycloakOrgEntityProviderOptions {\n /**\n * A unique, stable identifier for this provider.\n *\n * @example \"production\"\n */\n id: string;\n\n /**\n * The refresh schedule to use.\n * @remarks\n *\n * You can pass in the result of\n * {@link @backstage/backend-plugin-api#SchedulerService.createScheduledTaskRunner}\n * to enable automatic scheduling of tasks.\n */\n schedule?: SchedulerServiceTaskRunner;\n\n /**\n * Scheduler used to schedule refreshes based on\n * the schedule config.\n */\n scheduler?: SchedulerService;\n\n /**\n * The logger to use.\n */\n logger: LoggerService;\n\n /**\n * The function that transforms a user entry in LDAP to an entity.\n */\n userTransformer?: UserTransformer;\n\n /**\n * The function that transforms a group entry in LDAP to an entity.\n */\n groupTransformer?: GroupTransformer;\n}\n\n// Makes sure that emitted entities have a proper location\nexport const withLocations = (\n baseUrl: string,\n realm: string,\n entity: Entity,\n): Entity => {\n const kind = entity.kind === 'Group' ? 'groups' : 'users';\n const location = `url:${baseUrl}/admin/realms/${realm}/${kind}/${entity.metadata.annotations?.[KEYCLOAK_ID_ANNOTATION]}`;\n return merge(\n {\n metadata: {\n annotations: {\n [ANNOTATION_LOCATION]: location,\n [ANNOTATION_ORIGIN_LOCATION]: location,\n },\n },\n },\n entity,\n ) as Entity;\n};\n\n/**\n * Ingests org data (users and groups) from GitHub.\n *\n * @public\n */\nexport class KeycloakOrgEntityProvider implements EntityProvider {\n private connection?: EntityProviderConnection;\n private scheduleFn?: () => Promise<void>;\n\n /**\n * Static builder method to create multiple KeycloakOrgEntityProvider instances from a single config.\n * @param deps - The dependencies required for the provider, including the configuration and logger.\n * @param options - Options for scheduling tasks and transforming users and groups.\n * @returns An array of KeycloakOrgEntityProvider instances.\n */\n static fromConfig(\n deps: {\n config: Config;\n logger: LoggerService;\n },\n options: (\n | { schedule: SchedulerServiceTaskRunner }\n | { scheduler: SchedulerService }\n ) & {\n userTransformer?: UserTransformer;\n groupTransformer?: GroupTransformer;\n },\n ): KeycloakOrgEntityProvider[] {\n const { config, logger } = deps;\n return readProviderConfigs(config).map(providerConfig => {\n let taskRunner: SchedulerServiceTaskRunner | string;\n if ('scheduler' in options && providerConfig.schedule) {\n // Create a scheduled task runner using the provided scheduler and schedule configuration\n taskRunner = options.scheduler.createScheduledTaskRunner(\n providerConfig.schedule,\n );\n } else if ('schedule' in options) {\n // Use the provided schedule directly\n taskRunner = options.schedule;\n } else {\n throw new InputError(\n `No schedule provided via config for KeycloakOrgEntityProvider:${providerConfig.id}.`,\n );\n }\n\n const provider = new KeycloakOrgEntityProvider({\n id: providerConfig.id,\n provider: providerConfig,\n logger: logger,\n taskRunner: taskRunner,\n userTransformer: options.userTransformer,\n groupTransformer: options.groupTransformer,\n });\n\n return provider;\n });\n }\n\n constructor(\n private options: {\n id: string;\n provider: KeycloakProviderConfig;\n logger: LoggerService;\n taskRunner: SchedulerServiceTaskRunner;\n userTransformer?: UserTransformer;\n groupTransformer?: GroupTransformer;\n },\n ) {\n this.schedule(options.taskRunner);\n }\n\n /**\n * Returns the name of this entity provider.\n */\n getProviderName(): string {\n return `KeycloakOrgEntityProvider:${this.options.id}`;\n }\n\n /**\n * Connect to Backstage catalog entity provider\n * @param connection - The connection to the catalog API ingestor, which allows the provision of new entities.\n */\n async connect(connection: EntityProviderConnection) {\n this.connection = connection;\n await this.scheduleFn?.();\n }\n\n /**\n * Runs one complete ingestion loop. Call this method regularly at some\n * appropriate cadence.\n */\n async read(options?: { logger?: LoggerService }) {\n if (!this.connection) {\n throw new NotFoundError('Not initialized');\n }\n\n const logger = options?.logger ?? this.options.logger;\n const provider = this.options.provider;\n\n const { markReadComplete } = trackProgress(logger);\n const KeyCloakAdminClientModule = await inclusion(\n '@keycloak/keycloak-admin-client',\n );\n const KeyCloakAdminClient = KeyCloakAdminClientModule.default;\n\n const kcAdminClient = new KeyCloakAdminClient({\n baseUrl: provider.baseUrl,\n realmName: provider.loginRealm,\n });\n await authenticate(kcAdminClient, provider, logger);\n\n const pLimitCJSModule = await inclusion('p-limit');\n const limitFunc = pLimitCJSModule.default;\n const concurrency = provider.maxConcurrency ?? 20;\n const limit: LimitFunction = limitFunc(concurrency);\n\n const { users, groups } = await readKeycloakRealm(\n kcAdminClient,\n provider,\n logger,\n limit,\n {\n userQuerySize: provider.userQuerySize,\n groupQuerySize: provider.groupQuerySize,\n userTransformer: this.options.userTransformer,\n groupTransformer: this.options.groupTransformer,\n },\n );\n\n const { markCommitComplete } = markReadComplete({ users, groups });\n\n await this.connection.applyMutation({\n type: 'full',\n entities: [...users, ...groups].map(entity => ({\n locationKey: `keycloak-org-provider:${this.options.id}`,\n entity: withLocations(provider.baseUrl, provider.realm, entity),\n })),\n });\n\n markCommitComplete();\n }\n\n /**\n * Periodically schedules a task to read Keycloak user and group information, parse it, and provision it to the Backstage catalog.\n * @param taskRunner - The task runner to use for scheduling tasks.\n */\n schedule(taskRunner: SchedulerServiceTaskRunner) {\n this.scheduleFn = async () => {\n const id = `${this.getProviderName()}:refresh`;\n await taskRunner.run({\n id,\n fn: async () => {\n const logger = this.options.logger.child({\n class: KeycloakOrgEntityProvider.prototype.constructor.name,\n taskId: id,\n taskInstanceId: uuid.v4(),\n });\n\n try {\n await this.read({ logger });\n } catch (error) {\n if (isError(error)) {\n // Ensure that we don't log any sensitive internal data:\n logger.error('Error while syncing Keycloak users and groups', {\n // Default Error properties:\n name: error.name,\n cause: error.cause,\n message: error.message,\n stack: error.stack,\n // Additional status code if available:\n status: (error.response as { status?: string })?.status,\n });\n }\n }\n },\n });\n };\n }\n}\n\n// Helps wrap the timing and logging behaviors\nfunction trackProgress(logger: LoggerService) {\n let timestamp = Date.now();\n let summary: string;\n\n logger.info('Reading Keycloak users and groups');\n\n function markReadComplete(read: { users: unknown[]; groups: unknown[] }) {\n summary = `${read.users.length} Keycloak users and ${read.groups.length} Keycloak groups`;\n const readDuration = ((Date.now() - timestamp) / 1000).toFixed(1);\n timestamp = Date.now();\n logger.info(`Read ${summary} in ${readDuration} seconds. Committing...`);\n return { markCommitComplete };\n }\n\n function markCommitComplete() {\n const commitDuration = ((Date.now() - timestamp) / 1000).toFixed(1);\n logger.info(`Committed ${summary} in ${commitDuration} seconds.`);\n }\n\n return { markReadComplete };\n}\n"],"names":["KEYCLOAK_ID_ANNOTATION","merge","ANNOTATION_LOCATION","ANNOTATION_ORIGIN_LOCATION","config","readProviderConfigs","InputError","NotFoundError","inclusion","authenticate","readKeycloakRealm","uuid","isError"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backstage-community/plugin-catalog-backend-module-keycloak",
3
- "version": "3.3.0",
3
+ "version": "3.4.1",
4
4
  "description": "A Backend backend plugin for Keycloak",
5
5
  "main": "./dist/index.cjs.js",
6
6
  "types": "./dist/index.d.ts",
@@ -37,22 +37,23 @@
37
37
  "lint:fix": "backstage-cli package lint --fix",
38
38
  "postpack": "backstage-cli package postpack",
39
39
  "prepack": "backstage-cli package prepack",
40
- "start": "opener http://localhost:8080/admin/master/console/#/backstage-community-realm && opener http://localhost:7007/catalog/entities && turbo run start:plugin start:keycloak",
40
+ "start": "open-cli http://localhost:8080/admin/master/console/#/backstage-community-realm && open-cli http://localhost:7007/catalog/entities && yarn run start:plugin start:keycloak",
41
41
  "start:keycloak": "podman run -p 8080:8080 -e 'KEYCLOAK_ADMIN=admin' -e 'KEYCLOAK_ADMIN_PASSWORD=admin' -v ./__fixtures__/keycloak-realm.json:/opt/keycloak/data/import/keycloak-realm.json$([[ $OSTYPE = linux* ]] && echo ':z') quay.io/keycloak/keycloak:22.0.1 start-dev --import-realm",
42
42
  "start:plugin": "backstage-cli package start",
43
43
  "test": "backstage-cli package test --passWithNoTests --coverage",
44
- "tsc": "tsc",
45
- "prettier:check": "prettier --ignore-unknown --check .",
46
- "prettier:fix": "prettier --ignore-unknown --write ."
44
+ "tsc": "tsc"
47
45
  },
48
46
  "dependencies": {
49
47
  "@backstage/backend-plugin-api": "^1.1.0",
50
48
  "@backstage/catalog-model": "^1.7.2",
51
49
  "@backstage/errors": "^1.2.6",
52
50
  "@backstage/plugin-catalog-node": "^1.15.0",
53
- "@keycloak/keycloak-admin-client": "24.0.5",
51
+ "@common.js/p-limit": "^6.1.0",
52
+ "@keycloak/keycloak-admin-client": "26.1.0",
54
53
  "inclusion": "^1.0.1",
54
+ "jsonwebtoken": "^8.5.1",
55
55
  "lodash": "^4.17.21",
56
+ "p-limit": "^6.1.0",
56
57
  "pg-format": "^1.0.4",
57
58
  "uuid": "^9.0.1"
58
59
  },
@@ -66,7 +67,7 @@
66
67
  "@types/lodash": "4.17.13",
67
68
  "@types/uuid": "9.0.8",
68
69
  "deepmerge": "4.3.1",
69
- "prettier": "3.4.2"
70
+ "open-cli": "^8.0.0"
70
71
  },
71
72
  "files": [
72
73
  "dist",