@backstage/backend-defaults 0.17.8 → 0.17.9-next.0
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 +22 -12
- package/dist/entrypoints/auth/JwksClient.cjs.js +61 -32
- package/dist/entrypoints/auth/JwksClient.cjs.js.map +1 -1
- package/dist/entrypoints/auth/plugin/PluginTokenHandler.cjs.js +0 -1
- package/dist/entrypoints/auth/plugin/PluginTokenHandler.cjs.js.map +1 -1
- package/dist/entrypoints/auth/user/UserTokenHandler.cjs.js +0 -1
- package/dist/entrypoints/auth/user/UserTokenHandler.cjs.js.map +1 -1
- package/dist/entrypoints/database/DatabaseManager.cjs.js +12 -0
- package/dist/entrypoints/database/DatabaseManager.cjs.js.map +1 -1
- package/dist/entrypoints/database/connectors/postgres.cjs.js +216 -59
- package/dist/entrypoints/database/connectors/postgres.cjs.js.map +1 -1
- package/dist/entrypoints/urlReader/lib/FetchUrlReader.cjs.js +1 -0
- package/dist/entrypoints/urlReader/lib/FetchUrlReader.cjs.js.map +1 -1
- package/dist/entrypoints/urlReader/lib/HarnessUrlReader.cjs.js +58 -25
- package/dist/entrypoints/urlReader/lib/HarnessUrlReader.cjs.js.map +1 -1
- package/dist/package.json.cjs.js +1 -1
- package/dist/urlReader.d.ts +3 -0
- package/package.json +17 -17
package/CHANGELOG.md
CHANGED
|
@@ -1,17 +1,27 @@
|
|
|
1
1
|
# @backstage/backend-defaults
|
|
2
2
|
|
|
3
|
-
## 0.17.
|
|
4
|
-
|
|
5
|
-
### Patch Changes
|
|
6
|
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
|
|
3
|
+
## 0.17.9-next.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 45d760b: Improve action handling consistency.
|
|
8
|
+
- 8d71644: Reduced PostgreSQL connection churn during backend startup when many plugins initialize databases or schemas.
|
|
9
|
+
- 416be1b: Updated Harness URL reading to validate cross-origin redirect destinations
|
|
10
|
+
against `backend.reading.allow`.
|
|
11
|
+
- 5aac521: Improved service credential handling during plugin-to-plugin delegation.
|
|
12
|
+
- 61449ae: Reduced PostgreSQL startup connections when multiple plugins share a database by reusing the database existence check.
|
|
13
|
+
- 8b3c83e: Improved input validation and path handling for cloud storage URL readers.
|
|
14
|
+
- 61d97c0: Improved input validation for cloud storage URL readers.
|
|
15
|
+
- e895def: Fixed handling of GitLab URLs for instances configured with a relative base path.
|
|
16
|
+
- e13e278: Updated URL reader allow list matching so configured paths match either the exact path or paths below it at a segment boundary.
|
|
17
|
+
- e3b587c: Improve authentication reliability during signing key rotation by performing budgeted JWKS reloads when a newly published key is requested during the remote key set cooldown.
|
|
18
|
+
- Updated dependencies
|
|
19
|
+
- @backstage/integration@2.1.2-next.0
|
|
20
|
+
- @backstage/backend-app-api@1.7.4-next.0
|
|
21
|
+
- @backstage/plugin-auth-node@0.7.5-next.0
|
|
22
|
+
- @backstage/plugin-permission-node@0.11.4-next.0
|
|
23
|
+
- @backstage/backend-plugin-api@1.10.1-next.0
|
|
24
|
+
- @backstage/plugin-events-node@0.4.26-next.0
|
|
15
25
|
|
|
16
26
|
## 0.17.7
|
|
17
27
|
|
|
@@ -1,48 +1,77 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var errors = require('@backstage/errors');
|
|
4
3
|
var jose = require('jose');
|
|
5
4
|
|
|
6
|
-
const
|
|
5
|
+
const FORCED_RELOAD_LIMIT = 10;
|
|
6
|
+
const FORCED_RELOAD_WINDOW_MS = 6e4;
|
|
7
|
+
const KEY_STORE_CACHE_LIMIT = 100;
|
|
8
|
+
class JwksKeyStore {
|
|
9
|
+
#resolver;
|
|
10
|
+
#tryUseForcedReload;
|
|
11
|
+
constructor(endpoint, tryUseForcedReload) {
|
|
12
|
+
this.#resolver = jose.createRemoteJWKSet(endpoint);
|
|
13
|
+
this.#tryUseForcedReload = tryUseForcedReload;
|
|
14
|
+
}
|
|
15
|
+
getKey = async (protectedHeader, token) => {
|
|
16
|
+
const coolingDown = this.#resolver.coolingDown;
|
|
17
|
+
try {
|
|
18
|
+
return await this.#resolver(protectedHeader, token);
|
|
19
|
+
} catch (error) {
|
|
20
|
+
if (!(error instanceof jose.errors.JWKSNoMatchingKey)) {
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
if (!coolingDown || !await this.#reload()) {
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
return this.#resolver(protectedHeader, token);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
async #reload() {
|
|
30
|
+
if (!this.#resolver.reloading && !this.#tryUseForcedReload()) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
await this.#resolver.reload();
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
7
37
|
class JwksClient {
|
|
8
|
-
#
|
|
9
|
-
#
|
|
38
|
+
#keyStores = /* @__PURE__ */ new Map();
|
|
39
|
+
#forcedReloads = [];
|
|
10
40
|
getEndpoint;
|
|
11
41
|
constructor(getEndpoint) {
|
|
12
42
|
this.getEndpoint = getEndpoint;
|
|
13
43
|
}
|
|
14
44
|
get getKey() {
|
|
15
|
-
|
|
16
|
-
throw new errors.AuthenticationError(
|
|
17
|
-
"refreshKeyStore must be called before jwksClient.getKey"
|
|
18
|
-
);
|
|
19
|
-
}
|
|
20
|
-
return this.#keyStore;
|
|
45
|
+
return this.#getKey;
|
|
21
46
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
let
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
} catch (error) {
|
|
38
|
-
keyStoreHasKey = false;
|
|
47
|
+
#getKey = async (protectedHeader, token) => {
|
|
48
|
+
const keyStore = await this.#getKeyStore();
|
|
49
|
+
return keyStore.getKey(protectedHeader, token);
|
|
50
|
+
};
|
|
51
|
+
async #getKeyStore() {
|
|
52
|
+
const endpoint = await this.getEndpoint();
|
|
53
|
+
let keyStore = this.#keyStores.get(endpoint.href);
|
|
54
|
+
if (keyStore) {
|
|
55
|
+
this.#keyStores.delete(endpoint.href);
|
|
56
|
+
} else {
|
|
57
|
+
keyStore = new JwksKeyStore(endpoint, () => this.#tryUseForcedReload());
|
|
58
|
+
}
|
|
59
|
+
this.#keyStores.set(endpoint.href, keyStore);
|
|
60
|
+
if (this.#keyStores.size > KEY_STORE_CACHE_LIMIT) {
|
|
61
|
+
this.#keyStores.delete(this.#keyStores.keys().next().value);
|
|
39
62
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
63
|
+
return keyStore;
|
|
64
|
+
}
|
|
65
|
+
#tryUseForcedReload() {
|
|
66
|
+
const windowStart = Date.now() - FORCED_RELOAD_WINDOW_MS;
|
|
67
|
+
this.#forcedReloads = this.#forcedReloads.filter(
|
|
68
|
+
(timestamp) => timestamp > windowStart
|
|
69
|
+
);
|
|
70
|
+
if (this.#forcedReloads.length >= FORCED_RELOAD_LIMIT) {
|
|
71
|
+
return false;
|
|
45
72
|
}
|
|
73
|
+
this.#forcedReloads.push(Date.now());
|
|
74
|
+
return true;
|
|
46
75
|
}
|
|
47
76
|
}
|
|
48
77
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"JwksClient.cjs.js","sources":["../../../src/entrypoints/auth/JwksClient.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 {
|
|
1
|
+
{"version":3,"file":"JwksClient.cjs.js","sources":["../../../src/entrypoints/auth/JwksClient.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 {\n createRemoteJWKSet,\n errors,\n FlattenedJWSInput,\n GetKeyFunction,\n JWSHeaderParameters,\n} from 'jose';\n\nconst FORCED_RELOAD_LIMIT = 10;\nconst FORCED_RELOAD_WINDOW_MS = 60_000;\nconst KEY_STORE_CACHE_LIMIT = 100;\n\ntype RemoteJWKSet = ReturnType<typeof createRemoteJWKSet>;\n\nclass JwksKeyStore {\n readonly #resolver: RemoteJWKSet;\n readonly #tryUseForcedReload: () => boolean;\n\n constructor(endpoint: URL, tryUseForcedReload: () => boolean) {\n this.#resolver = createRemoteJWKSet(endpoint);\n this.#tryUseForcedReload = tryUseForcedReload;\n }\n\n getKey: GetKeyFunction<JWSHeaderParameters, FlattenedJWSInput> = async (\n protectedHeader,\n token,\n ) => {\n const coolingDown = this.#resolver.coolingDown;\n\n try {\n return await this.#resolver(protectedHeader, token);\n } catch (error) {\n if (!(error instanceof errors.JWKSNoMatchingKey)) {\n throw error;\n }\n\n if (!coolingDown || !(await this.#reload())) {\n throw error;\n }\n return this.#resolver(protectedHeader, token);\n }\n };\n\n async #reload(): Promise<boolean> {\n if (!this.#resolver.reloading && !this.#tryUseForcedReload()) {\n return false;\n }\n await this.#resolver.reload();\n return true;\n }\n}\n\nexport class JwksClient {\n #keyStores = new Map<string, JwksKeyStore>();\n #forcedReloads: number[] = [];\n\n private readonly getEndpoint: () => Promise<URL>;\n\n constructor(getEndpoint: () => Promise<URL>) {\n this.getEndpoint = getEndpoint;\n }\n\n get getKey() {\n return this.#getKey;\n }\n\n #getKey: GetKeyFunction<JWSHeaderParameters, FlattenedJWSInput> = async (\n protectedHeader,\n token,\n ) => {\n const keyStore = await this.#getKeyStore();\n return keyStore.getKey(protectedHeader, token);\n };\n\n async #getKeyStore(): Promise<JwksKeyStore> {\n const endpoint = await this.getEndpoint();\n let keyStore = this.#keyStores.get(endpoint.href);\n if (keyStore) {\n // Reinsert entries to keep the least recently used endpoint first.\n this.#keyStores.delete(endpoint.href);\n } else {\n keyStore = new JwksKeyStore(endpoint, () => this.#tryUseForcedReload());\n }\n this.#keyStores.set(endpoint.href, keyStore);\n if (this.#keyStores.size > KEY_STORE_CACHE_LIMIT) {\n this.#keyStores.delete(this.#keyStores.keys().next().value!);\n }\n return keyStore;\n }\n\n #tryUseForcedReload(): boolean {\n const windowStart = Date.now() - FORCED_RELOAD_WINDOW_MS;\n this.#forcedReloads = this.#forcedReloads.filter(\n timestamp => timestamp > windowStart,\n );\n if (this.#forcedReloads.length >= FORCED_RELOAD_LIMIT) {\n return false;\n }\n this.#forcedReloads.push(Date.now());\n return true;\n }\n}\n"],"names":["createRemoteJWKSet","errors"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PluginTokenHandler.cjs.js","sources":["../../../../src/entrypoints/auth/plugin/PluginTokenHandler.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 { DiscoveryService, LoggerService } from '@backstage/backend-plugin-api';\nimport { decodeJwt, importJWK, SignJWT, decodeProtectedHeader } from 'jose';\nimport { AuthenticationError, toError } from '@backstage/errors';\nimport { jwtVerify } from 'jose';\nimport { tokenTypes } from '@backstage/plugin-auth-node';\nimport { JwksClient } from '../JwksClient';\nimport { HumanDuration, durationToMilliseconds } from '@backstage/types';\nimport { PluginKeySource } from './keys/types';\n\nconst SECONDS_IN_MS = 1000;\n\nconst ALLOWED_PLUGIN_ID_PATTERN = /^[a-z0-9_-]+$/i;\n\ntype Options = {\n ownPluginId: string;\n keyDuration: HumanDuration;\n keySource: PluginKeySource;\n discovery: DiscoveryService;\n logger: LoggerService;\n /**\n * JWS \"alg\" (Algorithm) Header Parameter value. Defaults to ES256.\n * Must match one of the algorithms defined for IdentityClient.\n * When setting a different algorithm, check if the `key` field\n * of the `signing_keys` table can fit the length of the generated keys.\n * If not, add a knex migration file in the migrations folder.\n * More info on supported algorithms: https://github.com/panva/jose\n */\n algorithm?: string;\n};\n\n/**\n * @public\n * Issues and verifies {@link https://backstage.io/docs/auth/service-to-service-auth | service-to-service tokens}.\n */\nexport interface PluginTokenHandler {\n verifyToken(\n token: string,\n ): Promise<{ subject: string; limitedUserToken?: string } | undefined>;\n issueToken(options: {\n pluginId: string;\n targetPluginId: string;\n onBehalfOf?: { limitedUserToken: string; expiresAt: Date };\n }): Promise<{ token: string }>;\n}\n\nexport class DefaultPluginTokenHandler implements PluginTokenHandler {\n private jwksMap = new Map<string, JwksClient>();\n\n // Tracking state for isTargetPluginSupported\n private supportedTargetPlugins = new Set<string>();\n private targetPluginInflightChecks = new Map<string, Promise<boolean>>();\n\n static create(options: Options) {\n return new DefaultPluginTokenHandler(\n options.logger,\n options.ownPluginId,\n options.keySource,\n options.algorithm ?? 'ES256',\n Math.round(durationToMilliseconds(options.keyDuration) / 1000),\n options.discovery,\n );\n }\n\n private readonly logger: LoggerService;\n private readonly ownPluginId: string;\n private readonly keySource: PluginKeySource;\n private readonly algorithm: string;\n private readonly keyDurationSeconds: number;\n private readonly discovery: DiscoveryService;\n\n private constructor(\n logger: LoggerService,\n ownPluginId: string,\n keySource: PluginKeySource,\n algorithm: string,\n keyDurationSeconds: number,\n discovery: DiscoveryService,\n ) {\n this.logger = logger;\n this.ownPluginId = ownPluginId;\n this.keySource = keySource;\n this.algorithm = algorithm;\n this.keyDurationSeconds = keyDurationSeconds;\n this.discovery = discovery;\n }\n\n async verifyToken(\n token: string,\n ): Promise<{ subject: string; limitedUserToken?: string } | undefined> {\n try {\n const { typ } = decodeProtectedHeader(token);\n if (typ !== tokenTypes.plugin.typParam) {\n return undefined;\n }\n } catch {\n return undefined;\n }\n\n const pluginId = String(decodeJwt(token).sub);\n if (!pluginId) {\n throw new AuthenticationError('Invalid plugin token: missing subject');\n }\n if (!ALLOWED_PLUGIN_ID_PATTERN.test(pluginId)) {\n throw new AuthenticationError(\n 'Invalid plugin token: forbidden subject format',\n );\n }\n\n const jwksClient = await this.getJwksClient(pluginId);\n await jwksClient.refreshKeyStore(token); // TODO(Rugvip): Refactor so that this isn't needed\n\n const { payload } = await jwtVerify<{ sub: string; obo?: string }>(\n token,\n jwksClient.getKey,\n {\n typ: tokenTypes.plugin.typParam,\n audience: this.ownPluginId,\n requiredClaims: ['iat', 'exp', 'sub', 'aud'],\n },\n ).catch(e => {\n this.logger.warn('Failed to verify incoming plugin token', e);\n throw new AuthenticationError('Failed plugin token verification');\n });\n\n return { subject: `plugin:${payload.sub}`, limitedUserToken: payload.obo };\n }\n\n async issueToken(options: {\n pluginId: string;\n targetPluginId: string;\n onBehalfOf?: { limitedUserToken: string; expiresAt: Date };\n }): Promise<{ token: string }> {\n const { pluginId, targetPluginId, onBehalfOf } = options;\n const key = await this.keySource.getPrivateSigningKey();\n\n const sub = pluginId;\n const aud = targetPluginId;\n const iat = Math.floor(Date.now() / SECONDS_IN_MS);\n const ourExp = iat + this.keyDurationSeconds;\n const exp = onBehalfOf\n ? Math.min(\n ourExp,\n Math.floor(onBehalfOf.expiresAt.getTime() / SECONDS_IN_MS),\n )\n : ourExp;\n\n const claims = { sub, aud, iat, exp, obo: onBehalfOf?.limitedUserToken };\n const token = await new SignJWT(claims)\n .setProtectedHeader({\n typ: tokenTypes.plugin.typParam,\n alg: this.algorithm,\n kid: key.kid,\n })\n .setAudience(aud)\n .setSubject(sub)\n .setIssuedAt(iat)\n .setExpirationTime(exp)\n .sign(await importJWK(key));\n\n return { token };\n }\n\n private async isTargetPluginSupported(\n targetPluginId: string,\n ): Promise<boolean> {\n if (this.supportedTargetPlugins.has(targetPluginId)) {\n return true;\n }\n const inFlight = this.targetPluginInflightChecks.get(targetPluginId);\n if (inFlight) {\n return inFlight;\n }\n\n const doCheck = async () => {\n try {\n const res = await fetch(\n `${await this.discovery.getBaseUrl(\n targetPluginId,\n )}/.backstage/auth/v1/jwks.json`,\n );\n if (res.status === 404) {\n return false;\n }\n\n if (!res.ok) {\n throw new Error(`Failed to fetch jwks.json, ${res.status}`);\n }\n\n const data = await res.json();\n if (!data.keys) {\n throw new Error(`Invalid jwks.json response, missing keys`);\n }\n\n this.supportedTargetPlugins.add(targetPluginId);\n return true;\n } catch (error) {\n this.logger.error(\n 'Unexpected failure for target JWKS check',\n toError(error),\n );\n return false;\n } finally {\n this.targetPluginInflightChecks.delete(targetPluginId);\n }\n };\n\n const check = doCheck();\n this.targetPluginInflightChecks.set(targetPluginId, check);\n return check;\n }\n\n private async getJwksClient(pluginId: string) {\n const client = this.jwksMap.get(pluginId);\n if (client) {\n return client;\n }\n\n // Double check that the target plugin has a valid JWKS endpoint, otherwise avoid creating a remote key set\n if (!(await this.isTargetPluginSupported(pluginId))) {\n throw new AuthenticationError(\n `Received a plugin token where the source '${pluginId}' plugin unexpectedly does not have a JWKS endpoint. ` +\n 'The target plugin needs to be migrated to be installed in an app using the new backend system.',\n );\n }\n\n const newClient = new JwksClient(async () => {\n return new URL(\n `${await this.discovery.getBaseUrl(\n pluginId,\n )}/.backstage/auth/v1/jwks.json`,\n );\n });\n\n this.jwksMap.set(pluginId, newClient);\n return newClient;\n }\n}\n"],"names":["durationToMilliseconds","decodeProtectedHeader","tokenTypes","decodeJwt","AuthenticationError","jwtVerify","SignJWT","importJWK","toError","JwksClient"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"PluginTokenHandler.cjs.js","sources":["../../../../src/entrypoints/auth/plugin/PluginTokenHandler.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 { DiscoveryService, LoggerService } from '@backstage/backend-plugin-api';\nimport { decodeJwt, importJWK, SignJWT, decodeProtectedHeader } from 'jose';\nimport { AuthenticationError, toError } from '@backstage/errors';\nimport { jwtVerify } from 'jose';\nimport { tokenTypes } from '@backstage/plugin-auth-node';\nimport { JwksClient } from '../JwksClient';\nimport { HumanDuration, durationToMilliseconds } from '@backstage/types';\nimport { PluginKeySource } from './keys/types';\n\nconst SECONDS_IN_MS = 1000;\n\nconst ALLOWED_PLUGIN_ID_PATTERN = /^[a-z0-9_-]+$/i;\n\ntype Options = {\n ownPluginId: string;\n keyDuration: HumanDuration;\n keySource: PluginKeySource;\n discovery: DiscoveryService;\n logger: LoggerService;\n /**\n * JWS \"alg\" (Algorithm) Header Parameter value. Defaults to ES256.\n * Must match one of the algorithms defined for IdentityClient.\n * When setting a different algorithm, check if the `key` field\n * of the `signing_keys` table can fit the length of the generated keys.\n * If not, add a knex migration file in the migrations folder.\n * More info on supported algorithms: https://github.com/panva/jose\n */\n algorithm?: string;\n};\n\n/**\n * @public\n * Issues and verifies {@link https://backstage.io/docs/auth/service-to-service-auth | service-to-service tokens}.\n */\nexport interface PluginTokenHandler {\n verifyToken(\n token: string,\n ): Promise<{ subject: string; limitedUserToken?: string } | undefined>;\n issueToken(options: {\n pluginId: string;\n targetPluginId: string;\n onBehalfOf?: { limitedUserToken: string; expiresAt: Date };\n }): Promise<{ token: string }>;\n}\n\nexport class DefaultPluginTokenHandler implements PluginTokenHandler {\n private jwksMap = new Map<string, JwksClient>();\n\n // Tracking state for isTargetPluginSupported\n private supportedTargetPlugins = new Set<string>();\n private targetPluginInflightChecks = new Map<string, Promise<boolean>>();\n\n static create(options: Options) {\n return new DefaultPluginTokenHandler(\n options.logger,\n options.ownPluginId,\n options.keySource,\n options.algorithm ?? 'ES256',\n Math.round(durationToMilliseconds(options.keyDuration) / 1000),\n options.discovery,\n );\n }\n\n private readonly logger: LoggerService;\n private readonly ownPluginId: string;\n private readonly keySource: PluginKeySource;\n private readonly algorithm: string;\n private readonly keyDurationSeconds: number;\n private readonly discovery: DiscoveryService;\n\n private constructor(\n logger: LoggerService,\n ownPluginId: string,\n keySource: PluginKeySource,\n algorithm: string,\n keyDurationSeconds: number,\n discovery: DiscoveryService,\n ) {\n this.logger = logger;\n this.ownPluginId = ownPluginId;\n this.keySource = keySource;\n this.algorithm = algorithm;\n this.keyDurationSeconds = keyDurationSeconds;\n this.discovery = discovery;\n }\n\n async verifyToken(\n token: string,\n ): Promise<{ subject: string; limitedUserToken?: string } | undefined> {\n try {\n const { typ } = decodeProtectedHeader(token);\n if (typ !== tokenTypes.plugin.typParam) {\n return undefined;\n }\n } catch {\n return undefined;\n }\n\n const pluginId = String(decodeJwt(token).sub);\n if (!pluginId) {\n throw new AuthenticationError('Invalid plugin token: missing subject');\n }\n if (!ALLOWED_PLUGIN_ID_PATTERN.test(pluginId)) {\n throw new AuthenticationError(\n 'Invalid plugin token: forbidden subject format',\n );\n }\n\n const jwksClient = await this.getJwksClient(pluginId);\n\n const { payload } = await jwtVerify<{ sub: string; obo?: string }>(\n token,\n jwksClient.getKey,\n {\n typ: tokenTypes.plugin.typParam,\n audience: this.ownPluginId,\n requiredClaims: ['iat', 'exp', 'sub', 'aud'],\n },\n ).catch(e => {\n this.logger.warn('Failed to verify incoming plugin token', e);\n throw new AuthenticationError('Failed plugin token verification');\n });\n\n return { subject: `plugin:${payload.sub}`, limitedUserToken: payload.obo };\n }\n\n async issueToken(options: {\n pluginId: string;\n targetPluginId: string;\n onBehalfOf?: { limitedUserToken: string; expiresAt: Date };\n }): Promise<{ token: string }> {\n const { pluginId, targetPluginId, onBehalfOf } = options;\n const key = await this.keySource.getPrivateSigningKey();\n\n const sub = pluginId;\n const aud = targetPluginId;\n const iat = Math.floor(Date.now() / SECONDS_IN_MS);\n const ourExp = iat + this.keyDurationSeconds;\n const exp = onBehalfOf\n ? Math.min(\n ourExp,\n Math.floor(onBehalfOf.expiresAt.getTime() / SECONDS_IN_MS),\n )\n : ourExp;\n\n const claims = { sub, aud, iat, exp, obo: onBehalfOf?.limitedUserToken };\n const token = await new SignJWT(claims)\n .setProtectedHeader({\n typ: tokenTypes.plugin.typParam,\n alg: this.algorithm,\n kid: key.kid,\n })\n .setAudience(aud)\n .setSubject(sub)\n .setIssuedAt(iat)\n .setExpirationTime(exp)\n .sign(await importJWK(key));\n\n return { token };\n }\n\n private async isTargetPluginSupported(\n targetPluginId: string,\n ): Promise<boolean> {\n if (this.supportedTargetPlugins.has(targetPluginId)) {\n return true;\n }\n const inFlight = this.targetPluginInflightChecks.get(targetPluginId);\n if (inFlight) {\n return inFlight;\n }\n\n const doCheck = async () => {\n try {\n const res = await fetch(\n `${await this.discovery.getBaseUrl(\n targetPluginId,\n )}/.backstage/auth/v1/jwks.json`,\n );\n if (res.status === 404) {\n return false;\n }\n\n if (!res.ok) {\n throw new Error(`Failed to fetch jwks.json, ${res.status}`);\n }\n\n const data = await res.json();\n if (!data.keys) {\n throw new Error(`Invalid jwks.json response, missing keys`);\n }\n\n this.supportedTargetPlugins.add(targetPluginId);\n return true;\n } catch (error) {\n this.logger.error(\n 'Unexpected failure for target JWKS check',\n toError(error),\n );\n return false;\n } finally {\n this.targetPluginInflightChecks.delete(targetPluginId);\n }\n };\n\n const check = doCheck();\n this.targetPluginInflightChecks.set(targetPluginId, check);\n return check;\n }\n\n private async getJwksClient(pluginId: string) {\n const client = this.jwksMap.get(pluginId);\n if (client) {\n return client;\n }\n\n // Double check that the target plugin has a valid JWKS endpoint, otherwise avoid creating a remote key set\n if (!(await this.isTargetPluginSupported(pluginId))) {\n throw new AuthenticationError(\n `Received a plugin token where the source '${pluginId}' plugin unexpectedly does not have a JWKS endpoint. ` +\n 'The target plugin needs to be migrated to be installed in an app using the new backend system.',\n );\n }\n\n const newClient = new JwksClient(async () => {\n return new URL(\n `${await this.discovery.getBaseUrl(\n pluginId,\n )}/.backstage/auth/v1/jwks.json`,\n );\n });\n\n this.jwksMap.set(pluginId, newClient);\n return newClient;\n }\n}\n"],"names":["durationToMilliseconds","decodeProtectedHeader","tokenTypes","decodeJwt","AuthenticationError","jwtVerify","SignJWT","importJWK","toError","JwksClient"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"UserTokenHandler.cjs.js","sources":["../../../../src/entrypoints/auth/user/UserTokenHandler.ts"],"sourcesContent":["/*\n * Copyright 2020 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 { DiscoveryService, LoggerService } from '@backstage/backend-plugin-api';\nimport { AuthenticationError } from '@backstage/errors';\nimport { tokenTypes } from '@backstage/plugin-auth-node';\nimport {\n base64url,\n decodeJwt,\n decodeProtectedHeader,\n jwtVerify,\n JWTVerifyOptions,\n} from 'jose';\nimport { JwksClient } from '../JwksClient';\n\n/**\n * An identity client to interact with auth-backend and authenticate Backstage\n * tokens\n *\n * @internal\n */\nexport class UserTokenHandler {\n static create(options: {\n discovery: DiscoveryService;\n logger: LoggerService;\n }): UserTokenHandler {\n const jwksClient = new JwksClient(async () => {\n const url = await options.discovery.getBaseUrl('auth');\n return new URL(`${url}/.well-known/jwks.json`);\n });\n return new UserTokenHandler(jwksClient, options.logger);\n }\n\n private readonly jwksClient: JwksClient;\n private readonly logger: LoggerService;\n\n constructor(jwksClient: JwksClient, logger: LoggerService) {\n this.jwksClient = jwksClient;\n this.logger = logger;\n }\n\n async verifyToken(token: string) {\n const verifyOpts = this.#getTokenVerificationOptions(token);\n if (!verifyOpts) {\n return undefined;\n }\n\n
|
|
1
|
+
{"version":3,"file":"UserTokenHandler.cjs.js","sources":["../../../../src/entrypoints/auth/user/UserTokenHandler.ts"],"sourcesContent":["/*\n * Copyright 2020 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 { DiscoveryService, LoggerService } from '@backstage/backend-plugin-api';\nimport { AuthenticationError } from '@backstage/errors';\nimport { tokenTypes } from '@backstage/plugin-auth-node';\nimport {\n base64url,\n decodeJwt,\n decodeProtectedHeader,\n jwtVerify,\n JWTVerifyOptions,\n} from 'jose';\nimport { JwksClient } from '../JwksClient';\n\n/**\n * An identity client to interact with auth-backend and authenticate Backstage\n * tokens\n *\n * @internal\n */\nexport class UserTokenHandler {\n static create(options: {\n discovery: DiscoveryService;\n logger: LoggerService;\n }): UserTokenHandler {\n const jwksClient = new JwksClient(async () => {\n const url = await options.discovery.getBaseUrl('auth');\n return new URL(`${url}/.well-known/jwks.json`);\n });\n return new UserTokenHandler(jwksClient, options.logger);\n }\n\n private readonly jwksClient: JwksClient;\n private readonly logger: LoggerService;\n\n constructor(jwksClient: JwksClient, logger: LoggerService) {\n this.jwksClient = jwksClient;\n this.logger = logger;\n }\n\n async verifyToken(token: string) {\n const verifyOpts = this.#getTokenVerificationOptions(token);\n if (!verifyOpts) {\n return undefined;\n }\n\n // Verify a limited token, ensuring the necessarily claims are present and token type is correct\n const { payload } = await jwtVerify(\n token,\n this.jwksClient.getKey,\n verifyOpts,\n ).catch(e => {\n this.logger.warn('Failed to verify incoming user token', e);\n throw new AuthenticationError('Failed user token verification');\n });\n\n const userEntityRef = payload.sub;\n\n if (!userEntityRef) {\n throw new AuthenticationError('No user sub found in token');\n }\n\n return { userEntityRef };\n }\n\n #getTokenVerificationOptions(token: string): JWTVerifyOptions | undefined {\n try {\n const { typ } = decodeProtectedHeader(token);\n\n if (typ === tokenTypes.user.typParam) {\n return {\n requiredClaims: ['iat', 'exp', 'sub'],\n typ: tokenTypes.user.typParam,\n };\n }\n\n if (typ === tokenTypes.limitedUser.typParam) {\n return {\n requiredClaims: ['iat', 'exp', 'sub'],\n typ: tokenTypes.limitedUser.typParam,\n };\n }\n\n const { aud } = decodeJwt(token);\n if (aud === tokenTypes.user.audClaim) {\n return {\n audience: tokenTypes.user.audClaim,\n };\n }\n } catch {\n /* ignore */\n }\n\n return undefined;\n }\n\n createLimitedUserToken(backstageToken: string) {\n const [headerRaw, payloadRaw] = backstageToken.split('.');\n const header = JSON.parse(\n new TextDecoder().decode(base64url.decode(headerRaw)),\n );\n const payload = JSON.parse(\n new TextDecoder().decode(base64url.decode(payloadRaw)),\n );\n\n const tokenType = header.typ;\n\n // Only new user tokens can be used to create a limited user token. If we\n // can't create a limited token, or the token is already a limited one, we\n // return the original token\n if (!tokenType || tokenType === tokenTypes.limitedUser.typParam) {\n return { token: backstageToken, expiresAt: new Date(payload.exp * 1000) };\n }\n\n if (tokenType !== tokenTypes.user.typParam) {\n throw new AuthenticationError(\n 'Failed to create limited user token, invalid token type',\n );\n }\n\n if (!payload.uip) {\n throw new AuthenticationError(\n 'Failed to create limited user token, missing user identity proof',\n );\n }\n\n // NOTE: The order and properties in both the header and payload must match\n // the usage in plugins/auth-backend/src/identity/TokenFactory.ts\n const limitedUserToken = [\n base64url.encode(\n JSON.stringify({\n typ: tokenTypes.limitedUser.typParam,\n alg: header.alg,\n kid: header.kid,\n }),\n ),\n base64url.encode(\n JSON.stringify({\n sub: payload.sub,\n iat: payload.iat,\n exp: payload.exp,\n }),\n ),\n payload.uip,\n ].join('.');\n\n return { token: limitedUserToken, expiresAt: new Date(payload.exp * 1000) };\n }\n\n isLimitedUserToken(token: string): boolean {\n try {\n const { typ } = decodeProtectedHeader(token);\n return typ === tokenTypes.limitedUser.typParam;\n } catch {\n return false;\n }\n }\n}\n"],"names":["JwksClient","jwtVerify","AuthenticationError","decodeProtectedHeader","tokenTypes","decodeJwt","base64url"],"mappings":"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"}
|
|
@@ -68,6 +68,18 @@ class DatabaseManagerImpl {
|
|
|
68
68
|
}
|
|
69
69
|
})
|
|
70
70
|
);
|
|
71
|
+
const connectors = new Set(Object.values(this.connectors));
|
|
72
|
+
await Promise.all(
|
|
73
|
+
Array.from(connectors, async (connector) => {
|
|
74
|
+
try {
|
|
75
|
+
await connector.shutdown?.();
|
|
76
|
+
} catch (error) {
|
|
77
|
+
deps?.logger?.error(
|
|
78
|
+
`Problem closing database connector: ${errors.stringifyError(error)}`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
);
|
|
71
83
|
}
|
|
72
84
|
/**
|
|
73
85
|
* Provides the client type which should be used for a given plugin.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DatabaseManager.cjs.js","sources":["../../../src/entrypoints/database/DatabaseManager.ts"],"sourcesContent":["/*\n * Copyright 2021 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 {\n DatabaseService,\n LifecycleService,\n LoggerService,\n RootConfigService,\n RootLifecycleService,\n RootLoggerService,\n} from '@backstage/backend-plugin-api';\nimport { Config } from '@backstage/config';\nimport { stringifyError } from '@backstage/errors';\nimport { Knex } from 'knex';\nimport { MysqlConnector } from './connectors/mysql';\nimport { PgConnector } from './connectors/postgres';\nimport { Sqlite3Connector } from './connectors/sqlite3';\nimport { Connector } from './types';\n\n/**\n * Provides a config lookup path for a plugin's config block.\n */\nfunction pluginPath(pluginId: string): string {\n return `plugin.${pluginId}`;\n}\n\n/**\n * Creation options for {@link DatabaseManager}.\n *\n * @public\n */\nexport type DatabaseManagerOptions = {\n migrations?: DatabaseService['migrations'];\n rootLogger?: RootLoggerService;\n rootLifecycle?: RootLifecycleService;\n};\n\n/**\n * Testable implementation class for {@link DatabaseManager} below.\n */\nexport class DatabaseManagerImpl {\n private readonly config: Config;\n private readonly connectors: Record<string, Connector>;\n private readonly options?: DatabaseManagerOptions;\n private readonly databaseCache: Map<string, Promise<Knex>>;\n private readonly keepaliveIntervals: Map<string, NodeJS.Timeout>;\n\n constructor(\n config: Config,\n connectors: Record<string, Connector>,\n options?: DatabaseManagerOptions,\n databaseCache: Map<string, Promise<Knex>> = new Map(),\n keepaliveIntervals: Map<string, NodeJS.Timeout> = new Map(),\n ) {\n this.config = config;\n this.connectors = connectors;\n this.options = options;\n this.databaseCache = databaseCache;\n this.keepaliveIntervals = keepaliveIntervals;\n // If a rootLifecycle service was provided, register a shutdown hook to\n // clean up any database connections.\n if (options?.rootLifecycle !== undefined) {\n options.rootLifecycle.addShutdownHook(async () => {\n await this.shutdown({ logger: options.rootLogger });\n });\n }\n }\n\n /**\n * Generates a DatabaseService for consumption by plugins.\n *\n * @param pluginId - The plugin that the database manager should be created for. Plugin names\n * should be unique as they are used to look up database config overrides under\n * `backend.database.plugin`.\n */\n forPlugin(\n pluginId: string,\n deps: {\n logger: LoggerService;\n lifecycle: LifecycleService;\n },\n ): DatabaseService {\n const client = this.getClientType(pluginId).client;\n const connector = this.connectors[client];\n if (!connector) {\n throw new Error(\n `Unsupported database client type '${client}' specified for plugin '${pluginId}'`,\n );\n }\n const getClient = () => this.getDatabase(pluginId, connector, deps);\n\n const skip =\n this.options?.migrations?.skip ??\n this.config.getOptionalBoolean(`plugin.${pluginId}.skipMigrations`) ??\n this.config.getOptionalBoolean('skipMigrations') ??\n false;\n\n return { getClient, migrations: { skip } };\n }\n\n /**\n * Destroys all known connections.\n */\n private async shutdown(deps?: { logger?: LoggerService }): Promise<void> {\n const pluginIds = Array.from(this.databaseCache.keys());\n await Promise.allSettled(\n pluginIds.map(async pluginId => {\n // We no longer need to keep connections alive.\n clearInterval(this.keepaliveIntervals.get(pluginId));\n\n const connection = await this.databaseCache.get(pluginId);\n if (connection) {\n if (connection.client.config.includes('sqlite3')) {\n return; // sqlite3 does not support destroy, it hangs\n }\n await connection.destroy().catch((error: unknown) => {\n deps?.logger?.error(\n `Problem closing database connection for ${pluginId}: ${stringifyError(\n error,\n )}`,\n );\n });\n }\n }),\n );\n }\n\n /**\n * Provides the client type which should be used for a given plugin.\n *\n * The client type is determined by plugin specific config if present.\n * Otherwise the base client is used as the fallback.\n *\n * @param pluginId - Plugin to get the client type for\n * @returns Object with client type returned as `client` and boolean\n * representing whether or not the client was overridden as\n * `overridden`\n */\n private getClientType(pluginId: string): {\n client: string;\n overridden: boolean;\n } {\n const pluginClient = this.config.getOptionalString(\n `${pluginPath(pluginId)}.client`,\n );\n\n const baseClient = this.config.getString('client');\n const client = pluginClient ?? baseClient;\n return {\n client,\n overridden: client !== baseClient,\n };\n }\n\n /**\n * Provides a scoped Knex client for a plugin as per application config.\n *\n * @param pluginId - Plugin to get a Knex client for\n * @returns Promise which resolves to a scoped Knex database client for a\n * plugin\n */\n private async getDatabase(\n pluginId: string,\n connector: Connector,\n deps: {\n logger: LoggerService;\n lifecycle: LifecycleService;\n },\n ): Promise<Knex> {\n if (this.databaseCache.has(pluginId)) {\n return this.databaseCache.get(pluginId)!;\n }\n\n const clientPromise = connector.getClient(pluginId, deps);\n this.databaseCache.set(pluginId, clientPromise);\n\n if (process.env.NODE_ENV !== 'test') {\n clientPromise.then(client =>\n this.startKeepaliveLoop(pluginId, client, deps.logger),\n );\n }\n\n return clientPromise;\n }\n\n private startKeepaliveLoop(\n pluginId: string,\n client: Knex,\n logger: LoggerService,\n ): void {\n let lastKeepaliveFailed = false;\n\n this.keepaliveIntervals.set(\n pluginId,\n setInterval(() => {\n // During testing it can happen that the environment is torn down and\n // this client is `undefined`, but this interval is still run.\n client?.raw('select 1').then(\n () => {\n lastKeepaliveFailed = false;\n },\n (error: unknown) => {\n if (!lastKeepaliveFailed) {\n lastKeepaliveFailed = true;\n logger.warn(\n `Database keepalive failed for plugin ${pluginId}, ${stringifyError(\n error,\n )}`,\n );\n }\n },\n );\n }, 60 * 1000),\n );\n }\n}\n\n// NOTE: This class looks odd but is kept around for API compatibility reasons\n/**\n * Manages database connections for Backstage backend plugins.\n *\n * @public\n * @remarks\n *\n * The database manager allows the user to set connection and client settings on\n * a per pluginId basis by defining a database config block under\n * `plugin.<pluginId>` in addition to top level defaults. Optionally, a user may\n * set `prefix` which is used to prefix generated database names if config is\n * not provided.\n */\nexport class DatabaseManager {\n /**\n * Creates a {@link DatabaseManager} from `backend.database` config.\n *\n * @param config - The loaded application configuration.\n * @param options - An optional configuration object.\n */\n static fromConfig(\n config: RootConfigService,\n options?: DatabaseManagerOptions,\n ): DatabaseManager {\n const databaseConfig = config.getConfig('backend.database');\n const prefix =\n databaseConfig.getOptionalString('prefix') || 'backstage_plugin_';\n return new DatabaseManager(\n new DatabaseManagerImpl(\n databaseConfig,\n {\n pg: new PgConnector(databaseConfig, prefix),\n sqlite3: new Sqlite3Connector(databaseConfig),\n 'better-sqlite3': new Sqlite3Connector(databaseConfig),\n mysql: new MysqlConnector(databaseConfig, prefix),\n mysql2: new MysqlConnector(databaseConfig, prefix),\n },\n options,\n ),\n );\n }\n\n private readonly impl: DatabaseManagerImpl;\n\n private constructor(impl: DatabaseManagerImpl) {\n this.impl = impl;\n }\n\n /**\n * Generates a DatabaseService for consumption by plugins.\n *\n * @param pluginId - The plugin that the database manager should be created for. Plugin names\n * should be unique as they are used to look up database config overrides under\n * `backend.database.plugin`.\n */\n forPlugin(\n pluginId: string,\n deps: {\n logger: LoggerService;\n lifecycle: LifecycleService;\n },\n ): DatabaseService {\n return this.impl.forPlugin(pluginId, deps);\n }\n}\n"],"names":["stringifyError","PgConnector","Sqlite3Connector","MysqlConnector"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"DatabaseManager.cjs.js","sources":["../../../src/entrypoints/database/DatabaseManager.ts"],"sourcesContent":["/*\n * Copyright 2021 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 {\n DatabaseService,\n LifecycleService,\n LoggerService,\n RootConfigService,\n RootLifecycleService,\n RootLoggerService,\n} from '@backstage/backend-plugin-api';\nimport { Config } from '@backstage/config';\nimport { stringifyError } from '@backstage/errors';\nimport { Knex } from 'knex';\nimport { MysqlConnector } from './connectors/mysql';\nimport { PgConnector } from './connectors/postgres';\nimport { Sqlite3Connector } from './connectors/sqlite3';\nimport { Connector } from './types';\n\n/**\n * Provides a config lookup path for a plugin's config block.\n */\nfunction pluginPath(pluginId: string): string {\n return `plugin.${pluginId}`;\n}\n\n/**\n * Creation options for {@link DatabaseManager}.\n *\n * @public\n */\nexport type DatabaseManagerOptions = {\n migrations?: DatabaseService['migrations'];\n rootLogger?: RootLoggerService;\n rootLifecycle?: RootLifecycleService;\n};\n\n/**\n * Testable implementation class for {@link DatabaseManager} below.\n */\nexport class DatabaseManagerImpl {\n private readonly config: Config;\n private readonly connectors: Record<string, Connector>;\n private readonly options?: DatabaseManagerOptions;\n private readonly databaseCache: Map<string, Promise<Knex>>;\n private readonly keepaliveIntervals: Map<string, NodeJS.Timeout>;\n\n constructor(\n config: Config,\n connectors: Record<string, Connector>,\n options?: DatabaseManagerOptions,\n databaseCache: Map<string, Promise<Knex>> = new Map(),\n keepaliveIntervals: Map<string, NodeJS.Timeout> = new Map(),\n ) {\n this.config = config;\n this.connectors = connectors;\n this.options = options;\n this.databaseCache = databaseCache;\n this.keepaliveIntervals = keepaliveIntervals;\n // If a rootLifecycle service was provided, register a shutdown hook to\n // clean up any database connections.\n if (options?.rootLifecycle !== undefined) {\n options.rootLifecycle.addShutdownHook(async () => {\n await this.shutdown({ logger: options.rootLogger });\n });\n }\n }\n\n /**\n * Generates a DatabaseService for consumption by plugins.\n *\n * @param pluginId - The plugin that the database manager should be created for. Plugin names\n * should be unique as they are used to look up database config overrides under\n * `backend.database.plugin`.\n */\n forPlugin(\n pluginId: string,\n deps: {\n logger: LoggerService;\n lifecycle: LifecycleService;\n },\n ): DatabaseService {\n const client = this.getClientType(pluginId).client;\n const connector = this.connectors[client];\n if (!connector) {\n throw new Error(\n `Unsupported database client type '${client}' specified for plugin '${pluginId}'`,\n );\n }\n const getClient = () => this.getDatabase(pluginId, connector, deps);\n\n const skip =\n this.options?.migrations?.skip ??\n this.config.getOptionalBoolean(`plugin.${pluginId}.skipMigrations`) ??\n this.config.getOptionalBoolean('skipMigrations') ??\n false;\n\n return { getClient, migrations: { skip } };\n }\n\n /**\n * Destroys all known connections.\n */\n private async shutdown(deps?: { logger?: LoggerService }): Promise<void> {\n const pluginIds = Array.from(this.databaseCache.keys());\n await Promise.allSettled(\n pluginIds.map(async pluginId => {\n // We no longer need to keep connections alive.\n clearInterval(this.keepaliveIntervals.get(pluginId));\n\n const connection = await this.databaseCache.get(pluginId);\n if (connection) {\n if (connection.client.config.includes('sqlite3')) {\n return; // sqlite3 does not support destroy, it hangs\n }\n await connection.destroy().catch((error: unknown) => {\n deps?.logger?.error(\n `Problem closing database connection for ${pluginId}: ${stringifyError(\n error,\n )}`,\n );\n });\n }\n }),\n );\n\n const connectors = new Set(Object.values(this.connectors));\n await Promise.all(\n Array.from(connectors, async connector => {\n try {\n await connector.shutdown?.();\n } catch (error) {\n deps?.logger?.error(\n `Problem closing database connector: ${stringifyError(error)}`,\n );\n }\n }),\n );\n }\n\n /**\n * Provides the client type which should be used for a given plugin.\n *\n * The client type is determined by plugin specific config if present.\n * Otherwise the base client is used as the fallback.\n *\n * @param pluginId - Plugin to get the client type for\n * @returns Object with client type returned as `client` and boolean\n * representing whether or not the client was overridden as\n * `overridden`\n */\n private getClientType(pluginId: string): {\n client: string;\n overridden: boolean;\n } {\n const pluginClient = this.config.getOptionalString(\n `${pluginPath(pluginId)}.client`,\n );\n\n const baseClient = this.config.getString('client');\n const client = pluginClient ?? baseClient;\n return {\n client,\n overridden: client !== baseClient,\n };\n }\n\n /**\n * Provides a scoped Knex client for a plugin as per application config.\n *\n * @param pluginId - Plugin to get a Knex client for\n * @returns Promise which resolves to a scoped Knex database client for a\n * plugin\n */\n private async getDatabase(\n pluginId: string,\n connector: Connector,\n deps: {\n logger: LoggerService;\n lifecycle: LifecycleService;\n },\n ): Promise<Knex> {\n if (this.databaseCache.has(pluginId)) {\n return this.databaseCache.get(pluginId)!;\n }\n\n const clientPromise = connector.getClient(pluginId, deps);\n this.databaseCache.set(pluginId, clientPromise);\n\n if (process.env.NODE_ENV !== 'test') {\n clientPromise.then(client =>\n this.startKeepaliveLoop(pluginId, client, deps.logger),\n );\n }\n\n return clientPromise;\n }\n\n private startKeepaliveLoop(\n pluginId: string,\n client: Knex,\n logger: LoggerService,\n ): void {\n let lastKeepaliveFailed = false;\n\n this.keepaliveIntervals.set(\n pluginId,\n setInterval(() => {\n // During testing it can happen that the environment is torn down and\n // this client is `undefined`, but this interval is still run.\n client?.raw('select 1').then(\n () => {\n lastKeepaliveFailed = false;\n },\n (error: unknown) => {\n if (!lastKeepaliveFailed) {\n lastKeepaliveFailed = true;\n logger.warn(\n `Database keepalive failed for plugin ${pluginId}, ${stringifyError(\n error,\n )}`,\n );\n }\n },\n );\n }, 60 * 1000),\n );\n }\n}\n\n// NOTE: This class looks odd but is kept around for API compatibility reasons\n/**\n * Manages database connections for Backstage backend plugins.\n *\n * @public\n * @remarks\n *\n * The database manager allows the user to set connection and client settings on\n * a per pluginId basis by defining a database config block under\n * `plugin.<pluginId>` in addition to top level defaults. Optionally, a user may\n * set `prefix` which is used to prefix generated database names if config is\n * not provided.\n */\nexport class DatabaseManager {\n /**\n * Creates a {@link DatabaseManager} from `backend.database` config.\n *\n * @param config - The loaded application configuration.\n * @param options - An optional configuration object.\n */\n static fromConfig(\n config: RootConfigService,\n options?: DatabaseManagerOptions,\n ): DatabaseManager {\n const databaseConfig = config.getConfig('backend.database');\n const prefix =\n databaseConfig.getOptionalString('prefix') || 'backstage_plugin_';\n return new DatabaseManager(\n new DatabaseManagerImpl(\n databaseConfig,\n {\n pg: new PgConnector(databaseConfig, prefix),\n sqlite3: new Sqlite3Connector(databaseConfig),\n 'better-sqlite3': new Sqlite3Connector(databaseConfig),\n mysql: new MysqlConnector(databaseConfig, prefix),\n mysql2: new MysqlConnector(databaseConfig, prefix),\n },\n options,\n ),\n );\n }\n\n private readonly impl: DatabaseManagerImpl;\n\n private constructor(impl: DatabaseManagerImpl) {\n this.impl = impl;\n }\n\n /**\n * Generates a DatabaseService for consumption by plugins.\n *\n * @param pluginId - The plugin that the database manager should be created for. Plugin names\n * should be unique as they are used to look up database config overrides under\n * `backend.database.plugin`.\n */\n forPlugin(\n pluginId: string,\n deps: {\n logger: LoggerService;\n lifecycle: LifecycleService;\n },\n ): DatabaseService {\n return this.impl.forPlugin(pluginId, deps);\n }\n}\n"],"names":["stringifyError","PgConnector","Sqlite3Connector","MysqlConnector"],"mappings":"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"}
|