@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 +12 -0
- package/README.md +12 -11
- package/config.d.ts +5 -0
- package/dist/index.d.ts +41 -0
- package/dist/lib/authenticate.cjs.js +62 -0
- package/dist/lib/authenticate.cjs.js.map +1 -0
- package/dist/lib/config.cjs.js +3 -1
- package/dist/lib/config.cjs.js.map +1 -1
- package/dist/lib/read.cjs.js +150 -86
- package/dist/lib/read.cjs.js.map +1 -1
- package/dist/lib/transformers.cjs.js.map +1 -1
- package/dist/providers/KeycloakOrgEntityProvider.cjs.js +24 -20
- package/dist/providers/KeycloakOrgEntityProvider.cjs.js.map +1 -1
- package/package.json +8 -7
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
|
|
121
|
-
| ---------------- |
|
|
122
|
-
| `baseUrl` | Location of the Keycloak server, such as `https://localhost:8443`.
|
|
123
|
-
| `realm` | Realm to synchronize
|
|
124
|
-
| `loginRealm` | Realm used to authenticate
|
|
125
|
-
| `username` | Username to authenticate
|
|
126
|
-
| `password` | Password to authenticate
|
|
127
|
-
| `clientId` | Client ID to authenticate
|
|
128
|
-
| `clientSecret` | Client Secret to authenticate
|
|
129
|
-
| `userQuerySize` | Number of users to query at a time
|
|
130
|
-
| `groupQuerySize` | Number of groups to query at a time
|
|
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;;;;;"}
|
package/dist/lib/config.cjs.js
CHANGED
|
@@ -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":";;;;;
|
|
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;;;;"}
|
package/dist/lib/read.cjs.js
CHANGED
|
@@ -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:
|
|
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(
|
|
56
|
-
const
|
|
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) =>
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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(
|
|
162
|
-
g
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
g.
|
|
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
|
|
238
|
+
return null;
|
|
187
239
|
})
|
|
188
240
|
);
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
const
|
|
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
|
-
|
|
219
|
-
|
|
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
|
-
|
|
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;
|
package/dist/lib/read.cjs.js.map
CHANGED
|
@@ -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":"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"}
|
|
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":";;
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
+
"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": "
|
|
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
|
-
"@
|
|
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
|
-
"
|
|
70
|
+
"open-cli": "^8.0.0"
|
|
70
71
|
},
|
|
71
72
|
"files": [
|
|
72
73
|
"dist",
|