@backstage/backend-defaults 0.17.9-next.1 → 0.18.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 CHANGED
@@ -1,5 +1,41 @@
1
1
  # @backstage/backend-defaults
2
2
 
3
+ ## 0.18.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 2e07985: Add schemaPrefix configuration for pluginDivisionMode: schema. Allows prefixing PostgreSQL schema names to avoid conflicts with existing schemas.
8
+
9
+ ### Patch Changes
10
+
11
+ - fe6ace6: Fixed `AwsS3UrlReader` and `AwsCodeCommitUrlReader` to resolve account-specific AWS credentials when an assume role ARN is configured, enabling support for `webIdentityTokenFile` and `accountDefaults` in environments without default AWS credentials.
12
+ - 45d760b: Improve action handling consistency.
13
+ - 8d71644: Reduced PostgreSQL connection churn during backend startup when many plugins initialize databases or schemas.
14
+ - 416be1b: Updated Harness URL reading to validate cross-origin redirect destinations
15
+ against `backend.reading.allow`.
16
+ - c59472e: Plugin route registration now rejects paths that differ only by letter casing,
17
+ and HTTP credentials are resolved independently for each plugin service instance.
18
+ - 5aac521: Improved service credential handling during plugin-to-plugin delegation.
19
+ - 61449ae: Reduced PostgreSQL startup connections when multiple plugins share a database by reusing the database existence check.
20
+ - 55f30b8: Reduced scheduler database polling overhead by batching readiness checks for global tasks registered by each plugin into one query per poll cycle on each backend instance.
21
+ - 8b3c83e: Improved input validation and path handling for cloud storage URL readers.
22
+ - 61d97c0: Improved input validation for cloud storage URL readers.
23
+ - e895def: Fixed handling of GitLab URLs for instances configured with a relative base path.
24
+ - f6f339e: Fixed redundant signing-key requests when concurrently authenticating the first incoming requests from a plugin. Concurrent requests now share the same key cache, including when retrying after an initial fetch failure.
25
+ - e13e278: Updated URL reader allow list matching so configured paths match either the exact path or paths below it at a segment boundary.
26
+ - 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.
27
+ - Updated dependencies
28
+ - @backstage/integration@2.1.2
29
+ - @backstage/plugin-auth-node@0.7.6
30
+ - @backstage/config@1.3.9
31
+ - @backstage/backend-app-api@1.7.4
32
+ - @backstage/plugin-permission-node@0.11.4
33
+ - @backstage/backend-plugin-api@1.10.1
34
+ - @backstage/config-loader@1.11.3
35
+ - @backstage/integration-aws-node@0.2.2
36
+ - @backstage/plugin-events-node@0.4.26
37
+ - @backstage/plugin-permission-common@0.9.11
38
+
3
39
  ## 0.17.9-next.1
4
40
 
5
41
  ### Patch Changes
@@ -1053,6 +1053,11 @@
1053
1053
  "type": "string",
1054
1054
  "description": "Database name prefix override"
1055
1055
  },
1056
+ "schemaPrefix": {
1057
+ "type": "string",
1058
+ "description": "Prefix for schema names when using pluginDivisionMode: schema.",
1059
+ "default": ""
1060
+ },
1056
1061
  "ensureExists": {
1057
1062
  "type": "boolean",
1058
1063
  "description": "Whether to ensure the given database exists by creating it if it does not. Defaults to true if unspecified."
@@ -131,15 +131,15 @@ class DefaultPluginTokenHandler {
131
131
  return check;
132
132
  }
133
133
  async getJwksClient(pluginId) {
134
- const client = this.jwksMap.get(pluginId);
135
- if (client) {
136
- return client;
137
- }
138
- if (!await this.isTargetPluginSupported(pluginId)) {
134
+ if (!this.jwksMap.has(pluginId) && !await this.isTargetPluginSupported(pluginId)) {
139
135
  throw new errors.AuthenticationError(
140
136
  `Received a plugin token where the source '${pluginId}' plugin unexpectedly does not have a JWKS endpoint. The target plugin needs to be migrated to be installed in an app using the new backend system.`
141
137
  );
142
138
  }
139
+ const existingClient = this.jwksMap.get(pluginId);
140
+ if (existingClient) {
141
+ return existingClient;
142
+ }
143
143
  const newClient = new JwksClient.JwksClient(async () => {
144
144
  return new URL(
145
145
  `${await this.discovery.getBaseUrl(
@@ -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\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 // Double check that the target plugin has a valid JWKS endpoint, otherwise avoid creating a remote key set\n if (\n !this.jwksMap.has(pluginId) &&\n !(await this.isTargetPluginSupported(pluginId))\n ) {\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 // Concurrent callers may have populated the map while we checked support.\n // Reuse their client so that key fetches and refreshes stay deduplicated.\n const existingClient = this.jwksMap.get(pluginId);\n if (existingClient) {\n return existingClient;\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":";;;;;;;;AAyBA,MAAM,aAAA,GAAgB,GAAA;AAEtB,MAAM,yBAAA,GAA4B,gBAAA;AAkC3B,MAAM,yBAAA,CAAwD;AAAA,EAC3D,OAAA,uBAAc,GAAA,EAAwB;AAAA;AAAA,EAGtC,sBAAA,uBAA6B,GAAA,EAAY;AAAA,EACzC,0BAAA,uBAAiC,GAAA,EAA8B;AAAA,EAEvE,OAAO,OAAO,OAAA,EAAkB;AAC9B,IAAA,OAAO,IAAI,yBAAA;AAAA,MACT,OAAA,CAAQ,MAAA;AAAA,MACR,OAAA,CAAQ,WAAA;AAAA,MACR,OAAA,CAAQ,SAAA;AAAA,MACR,QAAQ,SAAA,IAAa,OAAA;AAAA,MACrB,KAAK,KAAA,CAAMA,4BAAA,CAAuB,OAAA,CAAQ,WAAW,IAAI,GAAI,CAAA;AAAA,MAC7D,OAAA,CAAQ;AAAA,KACV;AAAA,EACF;AAAA,EAEiB,MAAA;AAAA,EACA,WAAA;AAAA,EACA,SAAA;AAAA,EACA,SAAA;AAAA,EACA,kBAAA;AAAA,EACA,SAAA;AAAA,EAET,YACN,MAAA,EACA,WAAA,EACA,SAAA,EACA,SAAA,EACA,oBACA,SAAA,EACA;AACA,IAAA,IAAA,CAAK,MAAA,GAAS,MAAA;AACd,IAAA,IAAA,CAAK,WAAA,GAAc,WAAA;AACnB,IAAA,IAAA,CAAK,SAAA,GAAY,SAAA;AACjB,IAAA,IAAA,CAAK,SAAA,GAAY,SAAA;AACjB,IAAA,IAAA,CAAK,kBAAA,GAAqB,kBAAA;AAC1B,IAAA,IAAA,CAAK,SAAA,GAAY,SAAA;AAAA,EACnB;AAAA,EAEA,MAAM,YACJ,KAAA,EACqE;AACrE,IAAA,IAAI;AACF,MAAA,MAAM,EAAE,GAAA,EAAI,GAAIC,0BAAA,CAAsB,KAAK,CAAA;AAC3C,MAAA,IAAI,GAAA,KAAQC,yBAAA,CAAW,MAAA,CAAO,QAAA,EAAU;AACtC,QAAA,OAAO,KAAA,CAAA;AAAA,MACT;AAAA,IACF,CAAA,CAAA,MAAQ;AACN,MAAA,OAAO,MAAA;AAAA,IACT;AAEA,IAAA,MAAM,QAAA,GAAW,MAAA,CAAOC,cAAA,CAAU,KAAK,EAAE,GAAG,CAAA;AAC5C,IAAA,IAAI,CAAC,QAAA,EAAU;AACb,MAAA,MAAM,IAAIC,2BAAoB,uCAAuC,CAAA;AAAA,IACvE;AACA,IAAA,IAAI,CAAC,yBAAA,CAA0B,IAAA,CAAK,QAAQ,CAAA,EAAG;AAC7C,MAAA,MAAM,IAAIA,0BAAA;AAAA,QACR;AAAA,OACF;AAAA,IACF;AAEA,IAAA,MAAM,UAAA,GAAa,MAAM,IAAA,CAAK,aAAA,CAAc,QAAQ,CAAA;AAEpD,IAAA,MAAM,EAAE,OAAA,EAAQ,GAAI,MAAMC,cAAA;AAAA,MACxB,KAAA;AAAA,MACA,UAAA,CAAW,MAAA;AAAA,MACX;AAAA,QACE,GAAA,EAAKH,0BAAW,MAAA,CAAO,QAAA;AAAA,QACvB,UAAU,IAAA,CAAK,WAAA;AAAA,QACf,cAAA,EAAgB,CAAC,KAAA,EAAO,KAAA,EAAO,OAAO,KAAK;AAAA;AAC7C,KACF,CAAE,MAAM,CAAA,CAAA,KAAK;AACX,MAAA,IAAA,CAAK,MAAA,CAAO,IAAA,CAAK,wCAAA,EAA0C,CAAC,CAAA;AAC5D,MAAA,MAAM,IAAIE,2BAAoB,kCAAkC,CAAA;AAAA,IAClE,CAAC,CAAA;AAED,IAAA,OAAO,EAAE,SAAS,CAAA,OAAA,EAAU,OAAA,CAAQ,GAAG,CAAA,CAAA,EAAI,gBAAA,EAAkB,QAAQ,GAAA,EAAI;AAAA,EAC3E;AAAA,EAEA,MAAM,WAAW,OAAA,EAIc;AAC7B,IAAA,MAAM,EAAE,QAAA,EAAU,cAAA,EAAgB,UAAA,EAAW,GAAI,OAAA;AACjD,IAAA,MAAM,GAAA,GAAM,MAAM,IAAA,CAAK,SAAA,CAAU,oBAAA,EAAqB;AAEtD,IAAA,MAAM,GAAA,GAAM,QAAA;AACZ,IAAA,MAAM,GAAA,GAAM,cAAA;AACZ,IAAA,MAAM,MAAM,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,GAAA,KAAQ,aAAa,CAAA;AACjD,IAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,kBAAA;AAC1B,IAAA,MAAM,GAAA,GAAM,aACR,IAAA,CAAK,GAAA;AAAA,MACH,MAAA;AAAA,MACA,KAAK,KAAA,CAAM,UAAA,CAAW,SAAA,CAAU,OAAA,KAAY,aAAa;AAAA,KAC3D,GACA,MAAA;AAEJ,IAAA,MAAM,MAAA,GAAS,EAAE,GAAA,EAAK,GAAA,EAAK,KAAK,GAAA,EAAK,GAAA,EAAK,YAAY,gBAAA,EAAiB;AACvE,IAAA,MAAM,QAAQ,MAAM,IAAIE,YAAA,CAAQ,MAAM,EACnC,kBAAA,CAAmB;AAAA,MAClB,GAAA,EAAKJ,0BAAW,MAAA,CAAO,QAAA;AAAA,MACvB,KAAK,IAAA,CAAK,SAAA;AAAA,MACV,KAAK,GAAA,CAAI;AAAA,KACV,CAAA,CACA,WAAA,CAAY,GAAG,CAAA,CACf,UAAA,CAAW,GAAG,CAAA,CACd,WAAA,CAAY,GAAG,CAAA,CACf,kBAAkB,GAAG,CAAA,CACrB,KAAK,MAAMK,cAAA,CAAU,GAAG,CAAC,CAAA;AAE5B,IAAA,OAAO,EAAE,KAAA,EAAM;AAAA,EACjB;AAAA,EAEA,MAAc,wBACZ,cAAA,EACkB;AAClB,IAAA,IAAI,IAAA,CAAK,sBAAA,CAAuB,GAAA,CAAI,cAAc,CAAA,EAAG;AACnD,MAAA,OAAO,IAAA;AAAA,IACT;AACA,IAAA,MAAM,QAAA,GAAW,IAAA,CAAK,0BAAA,CAA2B,GAAA,CAAI,cAAc,CAAA;AACnE,IAAA,IAAI,QAAA,EAAU;AACZ,MAAA,OAAO,QAAA;AAAA,IACT;AAEA,IAAA,MAAM,UAAU,YAAY;AAC1B,MAAA,IAAI;AACF,QAAA,MAAM,MAAM,MAAM,KAAA;AAAA,UAChB,CAAA,EAAG,MAAM,IAAA,CAAK,SAAA,CAAU,UAAA;AAAA,YACtB;AAAA,WACD,CAAA,6BAAA;AAAA,SACH;AACA,QAAA,IAAI,GAAA,CAAI,WAAW,GAAA,EAAK;AACtB,UAAA,OAAO,KAAA;AAAA,QACT;AAEA,QAAA,IAAI,CAAC,IAAI,EAAA,EAAI;AACX,UAAA,MAAM,IAAI,KAAA,CAAM,CAAA,2BAAA,EAA8B,GAAA,CAAI,MAAM,CAAA,CAAE,CAAA;AAAA,QAC5D;AAEA,QAAA,MAAM,IAAA,GAAO,MAAM,GAAA,CAAI,IAAA,EAAK;AAC5B,QAAA,IAAI,CAAC,KAAK,IAAA,EAAM;AACd,UAAA,MAAM,IAAI,MAAM,CAAA,wCAAA,CAA0C,CAAA;AAAA,QAC5D;AAEA,QAAA,IAAA,CAAK,sBAAA,CAAuB,IAAI,cAAc,CAAA;AAC9C,QAAA,OAAO,IAAA;AAAA,MACT,SAAS,KAAA,EAAO;AACd,QAAA,IAAA,CAAK,MAAA,CAAO,KAAA;AAAA,UACV,0CAAA;AAAA,UACAC,eAAQ,KAAK;AAAA,SACf;AACA,QAAA,OAAO,KAAA;AAAA,MACT,CAAA,SAAE;AACA,QAAA,IAAA,CAAK,0BAAA,CAA2B,OAAO,cAAc,CAAA;AAAA,MACvD;AAAA,IACF,CAAA;AAEA,IAAA,MAAM,QAAQ,OAAA,EAAQ;AACtB,IAAA,IAAA,CAAK,0BAAA,CAA2B,GAAA,CAAI,cAAA,EAAgB,KAAK,CAAA;AACzD,IAAA,OAAO,KAAA;AAAA,EACT;AAAA,EAEA,MAAc,cAAc,QAAA,EAAkB;AAE5C,IAAA,IACE,CAAC,IAAA,CAAK,OAAA,CAAQ,GAAA,CAAI,QAAQ,CAAA,IAC1B,CAAE,MAAM,IAAA,CAAK,uBAAA,CAAwB,QAAQ,CAAA,EAC7C;AACA,MAAA,MAAM,IAAIJ,0BAAA;AAAA,QACR,6CAA6C,QAAQ,CAAA,mJAAA;AAAA,OAEvD;AAAA,IACF;AAIA,IAAA,MAAM,cAAA,GAAiB,IAAA,CAAK,OAAA,CAAQ,GAAA,CAAI,QAAQ,CAAA;AAChD,IAAA,IAAI,cAAA,EAAgB;AAClB,MAAA,OAAO,cAAA;AAAA,IACT;AAEA,IAAA,MAAM,SAAA,GAAY,IAAIK,qBAAA,CAAW,YAAY;AAC3C,MAAA,OAAO,IAAI,GAAA;AAAA,QACT,CAAA,EAAG,MAAM,IAAA,CAAK,SAAA,CAAU,UAAA;AAAA,UACtB;AAAA,SACD,CAAA,6BAAA;AAAA,OACH;AAAA,IACF,CAAC,CAAA;AAED,IAAA,IAAA,CAAK,OAAA,CAAQ,GAAA,CAAI,QAAA,EAAU,SAAS,CAAA;AACpC,IAAA,OAAO,SAAA;AAAA,EACT;AACF;;;;"}
@@ -157,11 +157,17 @@ class DatabaseManager {
157
157
  static fromConfig(config, options) {
158
158
  const databaseConfig = config.getConfig("backend.database");
159
159
  const prefix = databaseConfig.getOptionalString("prefix") || "backstage_plugin_";
160
+ const schemaPrefix = databaseConfig.getOptionalString("schemaPrefix") || "";
161
+ if (schemaPrefix && !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(schemaPrefix)) {
162
+ throw new Error(
163
+ `Invalid schemaPrefix "${schemaPrefix}". Schema prefix must start with a letter or underscore and contain only letters, numbers, and underscores.`
164
+ );
165
+ }
160
166
  return new DatabaseManager(
161
167
  new DatabaseManagerImpl(
162
168
  databaseConfig,
163
169
  {
164
- pg: new postgres.PgConnector(databaseConfig, prefix),
170
+ pg: new postgres.PgConnector(databaseConfig, prefix, schemaPrefix),
165
171
  sqlite3: new sqlite3.Sqlite3Connector(databaseConfig),
166
172
  "better-sqlite3": new sqlite3.Sqlite3Connector(databaseConfig),
167
173
  mysql: new mysql.MysqlConnector(databaseConfig, prefix),
@@ -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 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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"}
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 const schemaPrefix = databaseConfig.getOptionalString('schemaPrefix') || '';\n\n // Validate schemaPrefix contains only safe PostgreSQL identifier characters\n if (schemaPrefix && !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(schemaPrefix)) {\n throw new Error(\n `Invalid schemaPrefix \"${schemaPrefix}\". ` +\n `Schema prefix must start with a letter or underscore and contain only letters, numbers, and underscores.`,\n );\n }\n\n return new DatabaseManager(\n new DatabaseManagerImpl(\n databaseConfig,\n {\n pg: new PgConnector(databaseConfig, prefix, schemaPrefix),\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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"}
@@ -390,7 +390,7 @@ function normalizeConnection(connection) {
390
390
  }
391
391
  return typeof connection === "string" || connection instanceof String ? parsePgConnectionString(connection) : connection;
392
392
  }
393
- function computePgPluginConfig(config, pluginId, prefix) {
393
+ function computePgPluginConfig(config, pluginId, prefix, schemaPrefix = "") {
394
394
  const pluginClient = config.getOptionalString(
395
395
  `${pluginPath(pluginId)}.client`
396
396
  );
@@ -429,12 +429,19 @@ function computePgPluginConfig(config, pluginId, prefix) {
429
429
  databaseName = connectionDatabaseName ?? `${prefix}${pluginId}`;
430
430
  }
431
431
  let databaseClientOverrides = {};
432
+ let schemaName;
432
433
  if (databaseName) {
433
434
  databaseClientOverrides = { connection: { database: databaseName } };
434
435
  }
435
436
  if (pluginDivisionMode === "schema") {
437
+ schemaName = `${schemaPrefix}${pluginId}`;
438
+ if (Buffer.byteLength(schemaName, "utf8") > 63) {
439
+ throw new Error(
440
+ `PostgreSQL schema name "${schemaName}" exceeds the 63-byte limit. Consider using a shorter schemaPrefix (current: "${schemaPrefix}") or plugin ID (current: "${pluginId}").`
441
+ );
442
+ }
436
443
  databaseClientOverrides = mergeDatabaseConfig.mergeDatabaseConfig({}, databaseClientOverrides, {
437
- searchPath: [pluginId]
444
+ searchPath: [schemaName]
438
445
  });
439
446
  }
440
447
  const knexConfig = {
@@ -453,6 +460,7 @@ function computePgPluginConfig(config, pluginId, prefix) {
453
460
  pluginDivisionMode,
454
461
  connection,
455
462
  databaseName,
463
+ schemaName,
456
464
  databaseClientOverrides,
457
465
  knexConfig
458
466
  };
@@ -460,13 +468,15 @@ function computePgPluginConfig(config, pluginId, prefix) {
460
468
  class PgConnector {
461
469
  config;
462
470
  prefix;
471
+ schemaPrefix;
463
472
  databaseEnsureCache = /* @__PURE__ */ new Map();
464
473
  customEnsureDatabaseExists;
465
474
  databaseAdminPool;
466
475
  schemaAdminPool;
467
- constructor(config, prefix, options = {}) {
476
+ constructor(config, prefix, schemaPrefix, options = {}) {
468
477
  this.config = config;
469
478
  this.prefix = prefix;
479
+ this.schemaPrefix = schemaPrefix;
470
480
  this.customEnsureDatabaseExists = options.ensureDatabaseExists;
471
481
  const createAdminClient = options.createAdminClient ?? ((overrides) => createPgDatabaseClient(this.config, overrides));
472
482
  const idleTimeoutMillis = options.adminPoolIdleTimeoutMillis ?? 1e4;
@@ -532,7 +542,8 @@ class PgConnector {
532
542
  const pluginDbConfig = computePgPluginConfig(
533
543
  this.config,
534
544
  pluginId,
535
- this.prefix
545
+ this.prefix,
546
+ this.schemaPrefix
536
547
  );
537
548
  if (pluginDbConfig.databaseName && pluginDbConfig.ensureExists) {
538
549
  try {
@@ -552,7 +563,7 @@ class PgConnector {
552
563
  (admin) => ensurePgSchema(
553
564
  admin,
554
565
  this.config.getOptionalString("role"),
555
- pluginId
566
+ pluginDbConfig.schemaName
556
567
  )
557
568
  )
558
569
  );
@@ -1 +1 @@
1
- {"version":3,"file":"postgres.cjs.js","sources":["../../../../src/entrypoints/database/connectors/postgres.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 { LifecycleService, LoggerService } from '@backstage/backend-plugin-api';\nimport {\n Config,\n ConfigReader,\n readDurationFromConfig,\n} from '@backstage/config';\nimport { ForwardedError } from '@backstage/errors';\nimport {\n durationToMilliseconds,\n HumanDuration,\n JsonObject,\n} from '@backstage/types';\nimport knexFactory, { Knex } from 'knex';\nimport { merge, omit } from 'lodash';\nimport limiterFactory from 'p-limit';\nimport { Client } from 'pg';\nimport { Connector } from '../types';\nimport { mergeDatabaseConfig } from './mergeDatabaseConfig';\nimport format from 'pg-format';\nimport { TokenCredential } from '@azure/identity';\n\n// Limits the number of concurrent DDL operations to 1\nconst ddlLimiter = limiterFactory(1);\n\nexport class PgAdminPool {\n private client?: Promise<Knex>;\n private destruction?: Promise<void>;\n private readonly destructionErrors: unknown[] = [];\n private idleTimer?: NodeJS.Timeout;\n private readonly operations = new Set<Promise<unknown>>();\n private isShutdown = false;\n\n constructor(\n private readonly createClient: () => Promise<Knex>,\n private readonly idleTimeoutMillis: number,\n ) {}\n\n async run<T>(operation: (client: Knex) => Promise<T> | T): Promise<T> {\n if (this.isShutdown) {\n throw new Error('PostgreSQL admin pool is shut down');\n }\n\n clearTimeout(this.idleTimer);\n this.idleTimer = undefined;\n\n const operationPromise = this.runOperation(operation);\n this.operations.add(operationPromise);\n try {\n return await operationPromise;\n } finally {\n this.operations.delete(operationPromise);\n this.scheduleIdleDestroy();\n }\n }\n\n async shutdown(): Promise<void> {\n this.isShutdown = true;\n clearTimeout(this.idleTimer);\n this.idleTimer = undefined;\n\n await Promise.allSettled(this.operations);\n await this.destroyClient().catch(() => {});\n if (this.destructionErrors.length > 0) {\n throw new AggregateError(\n this.destructionErrors,\n 'Failed to destroy PostgreSQL admin pool clients',\n );\n }\n }\n\n async invalidate(): Promise<void> {\n clearTimeout(this.idleTimer);\n this.idleTimer = undefined;\n\n await Promise.allSettled(this.operations);\n await this.destroyClient();\n }\n\n private async runOperation<T>(\n operation: (client: Knex) => Promise<T> | T,\n ): Promise<T> {\n if (this.destruction) {\n await this.destruction;\n }\n if (this.isShutdown) {\n throw new Error('PostgreSQL admin pool is shut down');\n }\n\n let clientPromise = this.client;\n if (!clientPromise) {\n clientPromise = this.createClient();\n this.client = clientPromise;\n void clientPromise.catch(() => {\n if (this.client === clientPromise) {\n this.client = undefined;\n }\n });\n }\n return operation(await clientPromise);\n }\n\n private scheduleIdleDestroy(): void {\n if (this.isShutdown || this.operations.size > 0) {\n return;\n }\n\n const timer = setTimeout(() => {\n if (this.idleTimer === timer) {\n this.idleTimer = undefined;\n void this.destroyClient().catch(() => {});\n }\n }, this.idleTimeoutMillis);\n timer.unref();\n this.idleTimer = timer;\n }\n\n private destroyClient(): Promise<void> {\n if (this.destruction) {\n return this.destruction;\n }\n\n const clientPromise = this.client;\n this.client = undefined;\n if (!clientPromise) {\n return Promise.resolve();\n }\n\n const destruction = clientPromise.then(\n client => client.destroy(),\n () => undefined,\n );\n this.destruction = destruction;\n void destruction.then(\n () => {\n if (this.destruction === destruction) {\n this.destruction = undefined;\n }\n },\n error => {\n if (this.destruction === destruction) {\n this.destruction = undefined;\n this.destructionErrors.push(error);\n }\n },\n );\n return destruction;\n }\n}\n\n/**\n * Creates a knex postgres database connection\n *\n * @param dbConfig - The database config\n * @param overrides - Additional options to merge with the config\n */\nexport async function createPgDatabaseClient(\n dbConfig: Config,\n overrides?: Knex.Config,\n) {\n const knexConfig = await buildPgDatabaseConfig(dbConfig, overrides);\n const database = knexFactory(knexConfig);\n\n const role = dbConfig.getOptionalString('role');\n\n if (role) {\n database.client.pool.on(\n 'createSuccess',\n async (_event: number, pgClient: Client) => {\n const query = format('SET ROLE %I', role);\n await pgClient.query(query);\n },\n );\n }\n return database;\n}\n\n/**\n * Builds a knex postgres database connection\n *\n * @param dbConfig - The database config\n * @param overrides - Additional options to merge with the config\n */\nexport async function buildPgDatabaseConfig(\n dbConfig: Config,\n overrides?: Knex.Config,\n) {\n const config = mergeDatabaseConfig(\n dbConfig.get(),\n {\n connection: getPgConnectionConfig(dbConfig, !!overrides),\n useNullAsDefault: true,\n },\n overrides,\n );\n const mergedConfigReader = new ConfigReader(config);\n\n if (config.connection.type === 'default' || !config.connection.type) {\n const connectionValue = config.connection;\n const sanitizedConnection =\n typeof connectionValue === 'string' || connectionValue instanceof String\n ? connectionValue\n : // connection is an object, omit config-only props\n omit(connectionValue as Record<string, unknown>, [\n 'type',\n 'instance',\n 'tokenCredential',\n ]);\n\n return {\n ...config,\n connection: sanitizedConnection,\n };\n }\n\n switch (config.connection.type) {\n case 'azure':\n return buildAzurePgConfig(mergedConfigReader);\n case 'cloudsql':\n return buildCloudSqlConfig(mergedConfigReader);\n case 'rds':\n return buildRdsPgConfig(mergedConfigReader);\n default:\n throw new Error(`Unknown connection type: ${config.connection.type}`);\n }\n}\n\n/* Note: the following type definition is intentionally duplicated in\n * /packages/backend-defaults/config.d.ts so the clientSecret property\n * can be annotated with \"@visibility secret\" there.\n */\nexport type AzureTokenCredentialConfig = {\n /**\n * How early before an access token expires to refresh it with a new one.\n * Defaults to 5 minutes\n * Supported formats:\n * - A string in the format of '1d', '2 seconds' etc. as supported by the `ms`\n * library.\n * - A standard ISO formatted duration string, e.g. 'P2DT6H' or 'PT1M'.\n * - An object with individual units (in plural) as keys, e.g. `{ days: 2, hours: 6 }`.\n */\n tokenRenewableOffsetTime?: string | HumanDuration;\n /**\n * The client ID of a user-assigned managed identity.\n * If not provided, the system-assigned managed identity is used.\n */\n clientId?: string;\n clientSecret?: string;\n tenantId?: string;\n};\n\nexport async function buildAzurePgConfig(config: Config): Promise<Knex.Config> {\n const {\n DefaultAzureCredential,\n ManagedIdentityCredential,\n ClientSecretCredential,\n } = require('@azure/identity');\n\n const tokenConfig = config.getOptionalConfig('connection.tokenCredential');\n\n const tokenRenewableOffsetTime = durationToMilliseconds(\n tokenConfig?.has('tokenRenewableOffsetTime')\n ? readDurationFromConfig(tokenConfig, { key: 'tokenRenewableOffsetTime' })\n : { minutes: 5 },\n );\n\n const clientId = tokenConfig?.getOptionalString('clientId');\n const tenantId = tokenConfig?.getOptionalString('tenantId');\n const clientSecret = tokenConfig?.getOptionalString('clientSecret');\n let credential: TokenCredential;\n\n /**\n * Determine which TokenCredential to use based on provided config\n * 1. If clientId, tenantId and clientSecret are provided, use ClientSecretCredential\n * 2. If only clientId is provided, use ManagedIdentityCredential with user-assigned identity\n * 3. Otherwise, use DefaultAzureCredential (which may use system-assigned identity among other methods)\n */\n if (clientId && tenantId && clientSecret) {\n credential = new ClientSecretCredential(tenantId, clientId, clientSecret);\n } else if (clientId) {\n credential = new ManagedIdentityCredential(clientId);\n } else {\n credential = new DefaultAzureCredential();\n }\n\n const rawConfig = config.get() as Record<string, unknown>;\n\n const normalized = normalizeConnection(rawConfig.connection as any);\n const sanitizedConnection = omit(normalized, [\n 'type',\n 'instance',\n 'tokenCredential',\n ]) as Partial<Knex.StaticConnectionConfig>;\n\n async function getConnectionConfig() {\n const token = await credential.getToken(\n 'https://ossrdbms-aad.database.windows.net/.default',\n );\n\n if (!token) {\n throw new Error(\n 'Failed to acquire Azure access token for database authentication',\n );\n }\n\n const connectionConfig = {\n ...sanitizedConnection,\n password: token.token,\n expirationChecker: () =>\n /* return true if the token is within the renewable offset time */\n token.expiresOnTimestamp - tokenRenewableOffsetTime <= Date.now(),\n };\n\n return connectionConfig;\n }\n\n return {\n ...(rawConfig as Record<string, unknown>),\n connection: getConnectionConfig,\n };\n}\n\nexport async function buildCloudSqlConfig(\n config: Config,\n): Promise<Knex.Config> {\n const client = config.getOptionalString('client');\n\n if (client && client !== 'pg') {\n throw new Error('Cloud SQL only supports the pg client');\n }\n\n const instance = config.getOptionalString('connection.instance');\n if (!instance) {\n throw new Error('Missing instance connection name for Cloud SQL');\n }\n\n const {\n Connector: CloudSqlConnector,\n IpAddressTypes,\n AuthTypes,\n } = require('@google-cloud/cloud-sql-connector') as typeof import('@google-cloud/cloud-sql-connector');\n const connector = new CloudSqlConnector();\n\n type IpType = (typeof IpAddressTypes)[keyof typeof IpAddressTypes];\n const ipTypeRaw = config.getOptionalString('connection.ipAddressType');\n\n let ipType: IpType | undefined;\n if (ipTypeRaw !== undefined) {\n if (\n !(Object.values(IpAddressTypes) as Array<string | number>).includes(\n ipTypeRaw as any,\n )\n ) {\n throw new Error(\n `Invalid connection.ipAddressType: ${ipTypeRaw}; valid values: ${Object.values(\n IpAddressTypes,\n ).join(', ')}`,\n );\n }\n ipType = ipTypeRaw as unknown as IpType;\n }\n\n const clientOpts = await connector.getOptions({\n instanceConnectionName: instance,\n ipType: ipType ?? IpAddressTypes.PUBLIC,\n authType: AuthTypes.IAM,\n });\n\n const rawConfig = config.get() as Record<string, unknown>;\n const normalized = normalizeConnection(rawConfig.connection as any);\n const sanitizedConnection = omit(normalized, [\n 'type',\n 'instance',\n ]) as Partial<Knex.StaticConnectionConfig>;\n\n return {\n ...(rawConfig as Record<string, unknown>),\n client: 'pg',\n connection: {\n ...sanitizedConnection,\n ...clientOpts,\n },\n };\n}\n\nexport async function buildRdsPgConfig(config: Config): Promise<Knex.Config> {\n const { Signer } =\n require('@aws-sdk/rds-signer') as typeof import('@aws-sdk/rds-signer');\n\n let hostname: string;\n let port: number;\n let username: string;\n try {\n hostname = config.getString('connection.host');\n port = config.getNumber('connection.port');\n username = config.getString('connection.user');\n } catch (err) {\n throw new ForwardedError(\n 'AWS RDS IAM auth: missing required database connection config — make sure connection.host, connection.port, and connection.user are set and any environment variables they reference are set',\n err,\n );\n }\n const region =\n config.getOptionalString('connection.region') ??\n process.env.AWS_REGION ??\n process.env.AWS_DEFAULT_REGION;\n if (!region) {\n throw new Error(\n 'Missing region for AWS RDS IAM auth: set connection.region or the AWS_REGION environment variable',\n );\n }\n\n const rawConfig = config.get() as Record<string, unknown>;\n const sanitizedConnection = omit(\n config.get('connection') as Record<string, unknown>,\n ['type', 'region'],\n ) as Partial<Knex.StaticConnectionConfig>;\n\n const signer = new Signer({ hostname, port, username, region });\n\n // RDS IAM auth tokens are valid for 15 minutes. Renew 1 minute early so\n // that pooled connections are refreshed before the token actually expires.\n const tokenTtlMs = 15 * 60 * 1000;\n const renewalOffsetMs = 60 * 1000;\n\n async function getConnectionConfig() {\n try {\n const password = await signer.getAuthToken();\n const tokenExpiration = Date.now() + tokenTtlMs - renewalOffsetMs;\n return {\n ...sanitizedConnection,\n password,\n expirationChecker: () => tokenExpiration <= Date.now(),\n };\n } catch (err) {\n throw new ForwardedError(\n `AWS RDS IAM auth token acquisition failed for ${username}@${hostname}:${port}`,\n err,\n );\n }\n }\n\n return {\n ...(rawConfig as Record<string, unknown>),\n connection: getConnectionConfig,\n };\n}\n\n/**\n * Gets the postgres connection config\n *\n * @param dbConfig - The database config\n * @param parseConnectionString - Flag to explicitly control connection string parsing\n */\nexport function getPgConnectionConfig(\n dbConfig: Config,\n parseConnectionString?: boolean,\n): Knex.PgConnectionConfig | string {\n const connection = dbConfig.get('connection') as any;\n const isConnectionString =\n typeof connection === 'string' || connection instanceof String;\n const autoParse = typeof parseConnectionString !== 'boolean';\n\n const shouldParseConnectionString = autoParse\n ? isConnectionString\n : parseConnectionString && isConnectionString;\n\n return shouldParseConnectionString\n ? parsePgConnectionString(connection as string)\n : connection;\n}\n\n/**\n * Parses a connection string using pg-connection-string\n *\n * @param connectionString - The postgres connection string\n */\nexport function parsePgConnectionString(connectionString: string) {\n const parse = requirePgConnectionString();\n return parse(connectionString);\n}\n\nfunction requirePgConnectionString() {\n try {\n return require('pg-connection-string').parse;\n } catch (e) {\n throw new ForwardedError(\"Postgres: Install 'pg-connection-string'\", e);\n }\n}\n\nconst PG_DATABASE_ADMIN_OVERRIDES: Knex.Config = {\n connection: { database: 'postgres' },\n pool: { min: 0, max: 1, acquireTimeoutMillis: 10000 },\n};\n\nconst PG_SCHEMA_ADMIN_OVERRIDES: Knex.Config = {\n pool: { min: 0, max: 1, acquireTimeoutMillis: 10000 },\n};\n\n/**\n * Creates the missing Postgres database if it does not exist\n *\n * @param dbConfig - The database config\n * @param databases - The name of the databases to create\n */\nexport async function ensurePgDatabaseExists(\n dbConfig: Config,\n ...databases: Array<string>\n) {\n await Promise.all(\n databases.map(async database => {\n return retryPgAdminOperation(() =>\n ddlLimiter(async () => {\n const admin = await createPgDatabaseClient(\n dbConfig,\n PG_DATABASE_ADMIN_OVERRIDES,\n );\n try {\n await ensurePgDatabase(admin, database);\n } finally {\n await admin.destroy();\n }\n }),\n );\n }),\n );\n}\n\nasync function ensurePgDatabase(admin: Knex, database: string): Promise<void> {\n const result = await admin\n .from('pg_database')\n .where('datname', database)\n .count<Record<string, { count: string }>>();\n\n if (parseInt(result[0].count, 10) === 0) {\n await admin.raw(`CREATE DATABASE ??`, [database]);\n }\n}\n\nasync function retryPgAdminOperation(\n operation: () => Promise<void>,\n): Promise<void> {\n // For initial setup we use a smaller timeout but several retries. Given that this\n // is a separate connection pool we should never really run into issues with connection\n // acquisition timeouts, but we do anyway. This might be a bug in knex or some other dependency.\n const maxAttempts = 3;\n for (let attempt = 1; ; attempt++) {\n try {\n return await operation();\n } catch (error) {\n if (attempt >= maxAttempts) {\n throw error;\n }\n await new Promise(resolve => setTimeout(resolve, 100));\n }\n }\n}\n\nasync function ensurePgSchema(\n admin: Knex,\n role: string | undefined,\n schema: string,\n): Promise<void> {\n if (role) {\n await admin.raw(`CREATE SCHEMA IF NOT EXISTS ?? AUTHORIZATION ??`, [\n schema,\n role,\n ]);\n } else {\n await admin.raw(`CREATE SCHEMA IF NOT EXISTS ??`, [schema]);\n }\n}\n\n/**\n * Creates the missing Postgres schema if it does not exist\n *\n * @param dbConfig - The database config\n * @param schemas - The name of the schemas to create\n */\nexport async function ensurePgSchemaExists(\n dbConfig: Config,\n ...schemas: Array<string>\n): Promise<void> {\n const admin = await createPgDatabaseClient(dbConfig);\n const role = dbConfig.getOptionalString('role');\n\n try {\n await Promise.all(\n schemas.map(schema =>\n ddlLimiter(() => ensurePgSchema(admin, role, schema)),\n ),\n );\n } finally {\n await admin.destroy();\n }\n}\n\n/**\n * Drops the Postgres databases.\n *\n * @param dbConfig - The database config\n * @param databases - The name of the databases to drop\n */\nexport async function dropPgDatabase(\n dbConfig: Config,\n ...databases: Array<string>\n) {\n const admin = await createPgDatabaseClient(dbConfig);\n try {\n await Promise.all(\n databases.map(async database => {\n await ddlLimiter(() => admin.raw(`DROP DATABASE ??`, [database]));\n }),\n );\n } finally {\n await admin.destroy();\n }\n}\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\nfunction normalizeConnection(\n connection: Knex.StaticConnectionConfig | JsonObject | string | undefined,\n): Partial<Knex.StaticConnectionConfig> {\n if (typeof connection === 'undefined' || connection === null) {\n return {};\n }\n\n return typeof connection === 'string' || connection instanceof String\n ? parsePgConnectionString(connection as string)\n : connection;\n}\n\n/**\n * The computed configuration for a plugin's postgres database connection.\n */\nexport interface PgPluginDatabaseConfig {\n /** The database client type (e.g. 'pg') */\n client: string;\n /** Whether the client type was overridden at the plugin level */\n clientOverridden: boolean;\n /** The optional role to set on connections */\n role: string | undefined;\n /** Additional knex configuration merged from base and plugin config */\n additionalKnexConfig: JsonObject | undefined;\n /** Whether to ensure the database exists */\n ensureExists: boolean;\n /** Whether to ensure the schema exists */\n ensureSchemaExists: boolean;\n /** The plugin division mode ('database' or 'schema') */\n pluginDivisionMode: string;\n /** The connection configuration */\n connection: Knex.PgConnectionConfig;\n /** The database name, if any */\n databaseName: string | undefined;\n /** Database client overrides including schema overrides if applicable */\n databaseClientOverrides: Knex.Config;\n /** The full knex config for the plugin */\n knexConfig: Knex.Config;\n}\n\n/**\n * Computes all postgres database configuration for a plugin from the provided config.\n *\n * @param config - The database config object\n * @param pluginId - The plugin ID to compute config for\n * @param prefix - The database name prefix (e.g. 'backstage_plugin_')\n * @returns All computed configuration values for the plugin\n */\nexport function computePgPluginConfig(\n config: Config,\n pluginId: string,\n prefix: string,\n): PgPluginDatabaseConfig {\n // Client type\n const pluginClient = config.getOptionalString(\n `${pluginPath(pluginId)}.client`,\n );\n const baseClient = config.getString('client');\n const client = pluginClient ?? baseClient;\n const clientOverridden = client !== baseClient;\n\n // Role\n const role =\n config.getOptionalString(`${pluginPath(pluginId)}.role`) ??\n config.getOptionalString('role');\n\n // Additional knex config\n const pluginKnexConfig = config\n .getOptionalConfig(`${pluginPath(pluginId)}.knexConfig`)\n ?.get<JsonObject>();\n const baseKnexConfig = config\n .getOptionalConfig('knexConfig')\n ?.get<JsonObject>();\n const additionalKnexConfig = merge(baseKnexConfig, pluginKnexConfig);\n\n // Ensure exists flags\n const baseEnsureExists = config.getOptionalBoolean('ensureExists') ?? true;\n const ensureExists =\n config.getOptionalBoolean(`${pluginPath(pluginId)}.ensureExists`) ??\n baseEnsureExists;\n\n const baseEnsureSchemaExists =\n config.getOptionalBoolean('ensureSchemaExists') ?? false;\n const ensureSchemaExists =\n config.getOptionalBoolean(\n `${pluginPath(pluginId)}.getEnsureSchemaExistsConfig`,\n ) ?? baseEnsureSchemaExists;\n\n // Plugin division mode\n const pluginDivisionMode =\n config.getOptionalString('pluginDivisionMode') ?? 'database';\n\n // Connection config\n let baseConnection = normalizeConnection(config.get('connection'));\n\n // Databases cannot be shared unless the `pluginDivisionMode` is set to `schema`.\n // The `database` property from the base connection is omitted unless\n // `pluginDivisionMode` is set to `schema`.\n if (pluginDivisionMode !== 'schema') {\n baseConnection = omit(baseConnection, 'database');\n }\n\n // Get and normalize optional plugin specific database connection\n const pluginConnection = normalizeConnection(\n config.getOptional(`${pluginPath(pluginId)}.connection`),\n );\n\n (\n baseConnection as Knex.PgConnectionConfig\n ).application_name ||= `backstage_plugin_${pluginId}`;\n\n const connection = {\n // Include base connection if client type has not been overridden\n ...(clientOverridden ? {} : baseConnection),\n ...pluginConnection,\n } as Knex.PgConnectionConfig;\n\n // Database name\n const connectionDatabaseName = (connection as Knex.ConnectionConfig)\n ?.database;\n let databaseName: string | undefined;\n\n if (pluginDivisionMode === 'schema') {\n // `pluginDivisionMode` as `schema` should use overridden databaseName if supplied\n // or fallback to default knex database\n databaseName = connectionDatabaseName;\n } else {\n // All other supported databases should fallback to an auto-prefixed name\n databaseName = connectionDatabaseName ?? `${prefix}${pluginId}`;\n }\n\n // Database client overrides\n let databaseClientOverrides: Knex.Config = {};\n if (databaseName) {\n databaseClientOverrides = { connection: { database: databaseName } };\n }\n if (pluginDivisionMode === 'schema') {\n databaseClientOverrides = mergeDatabaseConfig({}, databaseClientOverrides, {\n searchPath: [pluginId],\n });\n }\n\n // Full knex config for plugin\n const knexConfig: Knex.Config = {\n ...additionalKnexConfig,\n client,\n connection,\n ...(role && { role }),\n };\n\n return {\n client,\n clientOverridden,\n role,\n additionalKnexConfig,\n ensureExists,\n ensureSchemaExists,\n pluginDivisionMode,\n connection,\n databaseName,\n databaseClientOverrides,\n knexConfig,\n };\n}\n\nexport class PgConnector implements Connector {\n private readonly config: Config;\n private readonly prefix: string;\n private readonly databaseEnsureCache = new Map<string, Promise<void>>();\n private readonly customEnsureDatabaseExists?: typeof ensurePgDatabaseExists;\n private readonly databaseAdminPool: PgAdminPool;\n private readonly schemaAdminPool: PgAdminPool;\n\n constructor(\n config: Config,\n prefix: string,\n options: {\n ensureDatabaseExists?: typeof ensurePgDatabaseExists;\n createAdminClient?: (overrides: Knex.Config) => Promise<Knex>;\n adminPoolIdleTimeoutMillis?: number;\n } = {},\n ) {\n this.config = config;\n this.prefix = prefix;\n this.customEnsureDatabaseExists = options.ensureDatabaseExists;\n\n const createAdminClient =\n options.createAdminClient ??\n ((overrides: Knex.Config) =>\n createPgDatabaseClient(this.config, overrides));\n const idleTimeoutMillis = options.adminPoolIdleTimeoutMillis ?? 10_000;\n this.databaseAdminPool = new PgAdminPool(\n () => createAdminClient(PG_DATABASE_ADMIN_OVERRIDES),\n idleTimeoutMillis,\n );\n this.schemaAdminPool = new PgAdminPool(\n () => createAdminClient(PG_SCHEMA_ADMIN_OVERRIDES),\n idleTimeoutMillis,\n );\n }\n\n private async ensureDatabase(databaseName: string): Promise<void> {\n let promise = this.databaseEnsureCache.get(databaseName);\n if (!promise) {\n promise = this.customEnsureDatabaseExists\n ? this.customEnsureDatabaseExists(this.config, databaseName)\n : retryPgAdminOperation(() =>\n ddlLimiter(() =>\n this.runAdminOperation(this.databaseAdminPool, admin =>\n ensurePgDatabase(admin, databaseName),\n ),\n ),\n );\n this.databaseEnsureCache.set(databaseName, promise);\n }\n\n try {\n await promise;\n } catch (error) {\n if (this.databaseEnsureCache.get(databaseName) === promise) {\n this.databaseEnsureCache.delete(databaseName);\n }\n throw error;\n }\n }\n\n private async runAdminOperation<T>(\n pool: PgAdminPool,\n operation: (client: Knex) => Promise<T>,\n ): Promise<T> {\n try {\n return await pool.run(operation);\n } catch (error) {\n await pool.invalidate().catch(() => {});\n throw error;\n }\n }\n\n async shutdown(): Promise<void> {\n const results = await Promise.allSettled([\n this.databaseAdminPool.shutdown(),\n this.schemaAdminPool.shutdown(),\n ]);\n const errors = results\n .filter(\n (result): result is PromiseRejectedResult =>\n result.status === 'rejected',\n )\n .map(result => result.reason);\n if (errors.length === 1) {\n throw errors[0];\n }\n if (errors.length > 1) {\n throw new AggregateError(\n errors,\n 'Failed to shut down PostgreSQL admin pools',\n );\n }\n }\n\n async getClient(\n pluginId: string,\n _deps: {\n logger: LoggerService;\n lifecycle: LifecycleService;\n },\n ): Promise<Knex> {\n const pluginDbConfig = computePgPluginConfig(\n this.config,\n pluginId,\n this.prefix,\n );\n\n if (pluginDbConfig.databaseName && pluginDbConfig.ensureExists) {\n try {\n await this.ensureDatabase(pluginDbConfig.databaseName);\n } catch (error) {\n throw new Error(\n `Failed to connect to the database to make sure that '${pluginDbConfig.databaseName}' exists, ${error}`,\n );\n }\n }\n\n if (pluginDbConfig.pluginDivisionMode === 'schema') {\n if (pluginDbConfig.ensureSchemaExists || pluginDbConfig.ensureExists) {\n try {\n await ddlLimiter(() =>\n this.runAdminOperation(this.schemaAdminPool, admin =>\n ensurePgSchema(\n admin,\n this.config.getOptionalString('role'),\n pluginId,\n ),\n ),\n );\n } catch (error) {\n throw new Error(\n `Failed to connect to the database to make sure that schema for plugin '${pluginId}' exists, ${error}`,\n );\n }\n }\n }\n\n const client = createPgDatabaseClient(\n this.config,\n mergeDatabaseConfig(\n pluginDbConfig.knexConfig,\n pluginDbConfig.databaseClientOverrides,\n ),\n );\n\n return client;\n }\n}\n"],"names":["limiterFactory","knexFactory","format","config","mergeDatabaseConfig","ConfigReader","omit","durationToMilliseconds","readDurationFromConfig","ForwardedError","merge"],"mappings":"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"}
1
+ {"version":3,"file":"postgres.cjs.js","sources":["../../../../src/entrypoints/database/connectors/postgres.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 { LifecycleService, LoggerService } from '@backstage/backend-plugin-api';\nimport {\n Config,\n ConfigReader,\n readDurationFromConfig,\n} from '@backstage/config';\nimport { ForwardedError } from '@backstage/errors';\nimport {\n durationToMilliseconds,\n HumanDuration,\n JsonObject,\n} from '@backstage/types';\nimport knexFactory, { Knex } from 'knex';\nimport { merge, omit } from 'lodash';\nimport limiterFactory from 'p-limit';\nimport { Client } from 'pg';\nimport { Connector } from '../types';\nimport { mergeDatabaseConfig } from './mergeDatabaseConfig';\nimport format from 'pg-format';\nimport { TokenCredential } from '@azure/identity';\n\n// Limits the number of concurrent DDL operations to 1\nconst ddlLimiter = limiterFactory(1);\n\nexport class PgAdminPool {\n private client?: Promise<Knex>;\n private destruction?: Promise<void>;\n private readonly destructionErrors: unknown[] = [];\n private idleTimer?: NodeJS.Timeout;\n private readonly operations = new Set<Promise<unknown>>();\n private isShutdown = false;\n\n constructor(\n private readonly createClient: () => Promise<Knex>,\n private readonly idleTimeoutMillis: number,\n ) {}\n\n async run<T>(operation: (client: Knex) => Promise<T> | T): Promise<T> {\n if (this.isShutdown) {\n throw new Error('PostgreSQL admin pool is shut down');\n }\n\n clearTimeout(this.idleTimer);\n this.idleTimer = undefined;\n\n const operationPromise = this.runOperation(operation);\n this.operations.add(operationPromise);\n try {\n return await operationPromise;\n } finally {\n this.operations.delete(operationPromise);\n this.scheduleIdleDestroy();\n }\n }\n\n async shutdown(): Promise<void> {\n this.isShutdown = true;\n clearTimeout(this.idleTimer);\n this.idleTimer = undefined;\n\n await Promise.allSettled(this.operations);\n await this.destroyClient().catch(() => {});\n if (this.destructionErrors.length > 0) {\n throw new AggregateError(\n this.destructionErrors,\n 'Failed to destroy PostgreSQL admin pool clients',\n );\n }\n }\n\n async invalidate(): Promise<void> {\n clearTimeout(this.idleTimer);\n this.idleTimer = undefined;\n\n await Promise.allSettled(this.operations);\n await this.destroyClient();\n }\n\n private async runOperation<T>(\n operation: (client: Knex) => Promise<T> | T,\n ): Promise<T> {\n if (this.destruction) {\n await this.destruction;\n }\n if (this.isShutdown) {\n throw new Error('PostgreSQL admin pool is shut down');\n }\n\n let clientPromise = this.client;\n if (!clientPromise) {\n clientPromise = this.createClient();\n this.client = clientPromise;\n void clientPromise.catch(() => {\n if (this.client === clientPromise) {\n this.client = undefined;\n }\n });\n }\n return operation(await clientPromise);\n }\n\n private scheduleIdleDestroy(): void {\n if (this.isShutdown || this.operations.size > 0) {\n return;\n }\n\n const timer = setTimeout(() => {\n if (this.idleTimer === timer) {\n this.idleTimer = undefined;\n void this.destroyClient().catch(() => {});\n }\n }, this.idleTimeoutMillis);\n timer.unref();\n this.idleTimer = timer;\n }\n\n private destroyClient(): Promise<void> {\n if (this.destruction) {\n return this.destruction;\n }\n\n const clientPromise = this.client;\n this.client = undefined;\n if (!clientPromise) {\n return Promise.resolve();\n }\n\n const destruction = clientPromise.then(\n client => client.destroy(),\n () => undefined,\n );\n this.destruction = destruction;\n void destruction.then(\n () => {\n if (this.destruction === destruction) {\n this.destruction = undefined;\n }\n },\n error => {\n if (this.destruction === destruction) {\n this.destruction = undefined;\n this.destructionErrors.push(error);\n }\n },\n );\n return destruction;\n }\n}\n\n/**\n * Creates a knex postgres database connection\n *\n * @param dbConfig - The database config\n * @param overrides - Additional options to merge with the config\n */\nexport async function createPgDatabaseClient(\n dbConfig: Config,\n overrides?: Knex.Config,\n) {\n const knexConfig = await buildPgDatabaseConfig(dbConfig, overrides);\n const database = knexFactory(knexConfig);\n\n const role = dbConfig.getOptionalString('role');\n\n if (role) {\n database.client.pool.on(\n 'createSuccess',\n async (_event: number, pgClient: Client) => {\n const query = format('SET ROLE %I', role);\n await pgClient.query(query);\n },\n );\n }\n return database;\n}\n\n/**\n * Builds a knex postgres database connection\n *\n * @param dbConfig - The database config\n * @param overrides - Additional options to merge with the config\n */\nexport async function buildPgDatabaseConfig(\n dbConfig: Config,\n overrides?: Knex.Config,\n) {\n const config = mergeDatabaseConfig(\n dbConfig.get(),\n {\n connection: getPgConnectionConfig(dbConfig, !!overrides),\n useNullAsDefault: true,\n },\n overrides,\n );\n const mergedConfigReader = new ConfigReader(config);\n\n if (config.connection.type === 'default' || !config.connection.type) {\n const connectionValue = config.connection;\n const sanitizedConnection =\n typeof connectionValue === 'string' || connectionValue instanceof String\n ? connectionValue\n : // connection is an object, omit config-only props\n omit(connectionValue as Record<string, unknown>, [\n 'type',\n 'instance',\n 'tokenCredential',\n ]);\n\n return {\n ...config,\n connection: sanitizedConnection,\n };\n }\n\n switch (config.connection.type) {\n case 'azure':\n return buildAzurePgConfig(mergedConfigReader);\n case 'cloudsql':\n return buildCloudSqlConfig(mergedConfigReader);\n case 'rds':\n return buildRdsPgConfig(mergedConfigReader);\n default:\n throw new Error(`Unknown connection type: ${config.connection.type}`);\n }\n}\n\n/* Note: the following type definition is intentionally duplicated in\n * /packages/backend-defaults/config.d.ts so the clientSecret property\n * can be annotated with \"@visibility secret\" there.\n */\nexport type AzureTokenCredentialConfig = {\n /**\n * How early before an access token expires to refresh it with a new one.\n * Defaults to 5 minutes\n * Supported formats:\n * - A string in the format of '1d', '2 seconds' etc. as supported by the `ms`\n * library.\n * - A standard ISO formatted duration string, e.g. 'P2DT6H' or 'PT1M'.\n * - An object with individual units (in plural) as keys, e.g. `{ days: 2, hours: 6 }`.\n */\n tokenRenewableOffsetTime?: string | HumanDuration;\n /**\n * The client ID of a user-assigned managed identity.\n * If not provided, the system-assigned managed identity is used.\n */\n clientId?: string;\n clientSecret?: string;\n tenantId?: string;\n};\n\nexport async function buildAzurePgConfig(config: Config): Promise<Knex.Config> {\n const {\n DefaultAzureCredential,\n ManagedIdentityCredential,\n ClientSecretCredential,\n } = require('@azure/identity');\n\n const tokenConfig = config.getOptionalConfig('connection.tokenCredential');\n\n const tokenRenewableOffsetTime = durationToMilliseconds(\n tokenConfig?.has('tokenRenewableOffsetTime')\n ? readDurationFromConfig(tokenConfig, { key: 'tokenRenewableOffsetTime' })\n : { minutes: 5 },\n );\n\n const clientId = tokenConfig?.getOptionalString('clientId');\n const tenantId = tokenConfig?.getOptionalString('tenantId');\n const clientSecret = tokenConfig?.getOptionalString('clientSecret');\n let credential: TokenCredential;\n\n /**\n * Determine which TokenCredential to use based on provided config\n * 1. If clientId, tenantId and clientSecret are provided, use ClientSecretCredential\n * 2. If only clientId is provided, use ManagedIdentityCredential with user-assigned identity\n * 3. Otherwise, use DefaultAzureCredential (which may use system-assigned identity among other methods)\n */\n if (clientId && tenantId && clientSecret) {\n credential = new ClientSecretCredential(tenantId, clientId, clientSecret);\n } else if (clientId) {\n credential = new ManagedIdentityCredential(clientId);\n } else {\n credential = new DefaultAzureCredential();\n }\n\n const rawConfig = config.get() as Record<string, unknown>;\n\n const normalized = normalizeConnection(rawConfig.connection as any);\n const sanitizedConnection = omit(normalized, [\n 'type',\n 'instance',\n 'tokenCredential',\n ]) as Partial<Knex.StaticConnectionConfig>;\n\n async function getConnectionConfig() {\n const token = await credential.getToken(\n 'https://ossrdbms-aad.database.windows.net/.default',\n );\n\n if (!token) {\n throw new Error(\n 'Failed to acquire Azure access token for database authentication',\n );\n }\n\n const connectionConfig = {\n ...sanitizedConnection,\n password: token.token,\n expirationChecker: () =>\n /* return true if the token is within the renewable offset time */\n token.expiresOnTimestamp - tokenRenewableOffsetTime <= Date.now(),\n };\n\n return connectionConfig;\n }\n\n return {\n ...(rawConfig as Record<string, unknown>),\n connection: getConnectionConfig,\n };\n}\n\nexport async function buildCloudSqlConfig(\n config: Config,\n): Promise<Knex.Config> {\n const client = config.getOptionalString('client');\n\n if (client && client !== 'pg') {\n throw new Error('Cloud SQL only supports the pg client');\n }\n\n const instance = config.getOptionalString('connection.instance');\n if (!instance) {\n throw new Error('Missing instance connection name for Cloud SQL');\n }\n\n const {\n Connector: CloudSqlConnector,\n IpAddressTypes,\n AuthTypes,\n } = require('@google-cloud/cloud-sql-connector') as typeof import('@google-cloud/cloud-sql-connector');\n const connector = new CloudSqlConnector();\n\n type IpType = (typeof IpAddressTypes)[keyof typeof IpAddressTypes];\n const ipTypeRaw = config.getOptionalString('connection.ipAddressType');\n\n let ipType: IpType | undefined;\n if (ipTypeRaw !== undefined) {\n if (\n !(Object.values(IpAddressTypes) as Array<string | number>).includes(\n ipTypeRaw as any,\n )\n ) {\n throw new Error(\n `Invalid connection.ipAddressType: ${ipTypeRaw}; valid values: ${Object.values(\n IpAddressTypes,\n ).join(', ')}`,\n );\n }\n ipType = ipTypeRaw as unknown as IpType;\n }\n\n const clientOpts = await connector.getOptions({\n instanceConnectionName: instance,\n ipType: ipType ?? IpAddressTypes.PUBLIC,\n authType: AuthTypes.IAM,\n });\n\n const rawConfig = config.get() as Record<string, unknown>;\n const normalized = normalizeConnection(rawConfig.connection as any);\n const sanitizedConnection = omit(normalized, [\n 'type',\n 'instance',\n ]) as Partial<Knex.StaticConnectionConfig>;\n\n return {\n ...(rawConfig as Record<string, unknown>),\n client: 'pg',\n connection: {\n ...sanitizedConnection,\n ...clientOpts,\n },\n };\n}\n\nexport async function buildRdsPgConfig(config: Config): Promise<Knex.Config> {\n const { Signer } =\n require('@aws-sdk/rds-signer') as typeof import('@aws-sdk/rds-signer');\n\n let hostname: string;\n let port: number;\n let username: string;\n try {\n hostname = config.getString('connection.host');\n port = config.getNumber('connection.port');\n username = config.getString('connection.user');\n } catch (err) {\n throw new ForwardedError(\n 'AWS RDS IAM auth: missing required database connection config — make sure connection.host, connection.port, and connection.user are set and any environment variables they reference are set',\n err,\n );\n }\n const region =\n config.getOptionalString('connection.region') ??\n process.env.AWS_REGION ??\n process.env.AWS_DEFAULT_REGION;\n if (!region) {\n throw new Error(\n 'Missing region for AWS RDS IAM auth: set connection.region or the AWS_REGION environment variable',\n );\n }\n\n const rawConfig = config.get() as Record<string, unknown>;\n const sanitizedConnection = omit(\n config.get('connection') as Record<string, unknown>,\n ['type', 'region'],\n ) as Partial<Knex.StaticConnectionConfig>;\n\n const signer = new Signer({ hostname, port, username, region });\n\n // RDS IAM auth tokens are valid for 15 minutes. Renew 1 minute early so\n // that pooled connections are refreshed before the token actually expires.\n const tokenTtlMs = 15 * 60 * 1000;\n const renewalOffsetMs = 60 * 1000;\n\n async function getConnectionConfig() {\n try {\n const password = await signer.getAuthToken();\n const tokenExpiration = Date.now() + tokenTtlMs - renewalOffsetMs;\n return {\n ...sanitizedConnection,\n password,\n expirationChecker: () => tokenExpiration <= Date.now(),\n };\n } catch (err) {\n throw new ForwardedError(\n `AWS RDS IAM auth token acquisition failed for ${username}@${hostname}:${port}`,\n err,\n );\n }\n }\n\n return {\n ...(rawConfig as Record<string, unknown>),\n connection: getConnectionConfig,\n };\n}\n\n/**\n * Gets the postgres connection config\n *\n * @param dbConfig - The database config\n * @param parseConnectionString - Flag to explicitly control connection string parsing\n */\nexport function getPgConnectionConfig(\n dbConfig: Config,\n parseConnectionString?: boolean,\n): Knex.PgConnectionConfig | string {\n const connection = dbConfig.get('connection') as any;\n const isConnectionString =\n typeof connection === 'string' || connection instanceof String;\n const autoParse = typeof parseConnectionString !== 'boolean';\n\n const shouldParseConnectionString = autoParse\n ? isConnectionString\n : parseConnectionString && isConnectionString;\n\n return shouldParseConnectionString\n ? parsePgConnectionString(connection as string)\n : connection;\n}\n\n/**\n * Parses a connection string using pg-connection-string\n *\n * @param connectionString - The postgres connection string\n */\nexport function parsePgConnectionString(connectionString: string) {\n const parse = requirePgConnectionString();\n return parse(connectionString);\n}\n\nfunction requirePgConnectionString() {\n try {\n return require('pg-connection-string').parse;\n } catch (e) {\n throw new ForwardedError(\"Postgres: Install 'pg-connection-string'\", e);\n }\n}\n\nconst PG_DATABASE_ADMIN_OVERRIDES: Knex.Config = {\n connection: { database: 'postgres' },\n pool: { min: 0, max: 1, acquireTimeoutMillis: 10000 },\n};\n\nconst PG_SCHEMA_ADMIN_OVERRIDES: Knex.Config = {\n pool: { min: 0, max: 1, acquireTimeoutMillis: 10000 },\n};\n\n/**\n * Creates the missing Postgres database if it does not exist\n *\n * @param dbConfig - The database config\n * @param databases - The name of the databases to create\n */\nexport async function ensurePgDatabaseExists(\n dbConfig: Config,\n ...databases: Array<string>\n) {\n await Promise.all(\n databases.map(async database => {\n return retryPgAdminOperation(() =>\n ddlLimiter(async () => {\n const admin = await createPgDatabaseClient(\n dbConfig,\n PG_DATABASE_ADMIN_OVERRIDES,\n );\n try {\n await ensurePgDatabase(admin, database);\n } finally {\n await admin.destroy();\n }\n }),\n );\n }),\n );\n}\n\nasync function ensurePgDatabase(admin: Knex, database: string): Promise<void> {\n const result = await admin\n .from('pg_database')\n .where('datname', database)\n .count<Record<string, { count: string }>>();\n\n if (parseInt(result[0].count, 10) === 0) {\n await admin.raw(`CREATE DATABASE ??`, [database]);\n }\n}\n\nasync function retryPgAdminOperation(\n operation: () => Promise<void>,\n): Promise<void> {\n // For initial setup we use a smaller timeout but several retries. Given that this\n // is a separate connection pool we should never really run into issues with connection\n // acquisition timeouts, but we do anyway. This might be a bug in knex or some other dependency.\n const maxAttempts = 3;\n for (let attempt = 1; ; attempt++) {\n try {\n return await operation();\n } catch (error) {\n if (attempt >= maxAttempts) {\n throw error;\n }\n await new Promise(resolve => setTimeout(resolve, 100));\n }\n }\n}\n\nasync function ensurePgSchema(\n admin: Knex,\n role: string | undefined,\n schema: string,\n): Promise<void> {\n if (role) {\n await admin.raw(`CREATE SCHEMA IF NOT EXISTS ?? AUTHORIZATION ??`, [\n schema,\n role,\n ]);\n } else {\n await admin.raw(`CREATE SCHEMA IF NOT EXISTS ??`, [schema]);\n }\n}\n\n/**\n * Creates the missing Postgres schema if it does not exist\n *\n * @param dbConfig - The database config\n * @param schemas - The name of the schemas to create\n */\nexport async function ensurePgSchemaExists(\n dbConfig: Config,\n ...schemas: Array<string>\n): Promise<void> {\n const admin = await createPgDatabaseClient(dbConfig);\n const role = dbConfig.getOptionalString('role');\n\n try {\n await Promise.all(\n schemas.map(schema =>\n ddlLimiter(() => ensurePgSchema(admin, role, schema)),\n ),\n );\n } finally {\n await admin.destroy();\n }\n}\n\n/**\n * Drops the Postgres databases.\n *\n * @param dbConfig - The database config\n * @param databases - The name of the databases to drop\n */\nexport async function dropPgDatabase(\n dbConfig: Config,\n ...databases: Array<string>\n) {\n const admin = await createPgDatabaseClient(dbConfig);\n try {\n await Promise.all(\n databases.map(async database => {\n await ddlLimiter(() => admin.raw(`DROP DATABASE ??`, [database]));\n }),\n );\n } finally {\n await admin.destroy();\n }\n}\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\nfunction normalizeConnection(\n connection: Knex.StaticConnectionConfig | JsonObject | string | undefined,\n): Partial<Knex.StaticConnectionConfig> {\n if (typeof connection === 'undefined' || connection === null) {\n return {};\n }\n\n return typeof connection === 'string' || connection instanceof String\n ? parsePgConnectionString(connection as string)\n : connection;\n}\n\n/**\n * The computed configuration for a plugin's postgres database connection.\n */\nexport interface PgPluginDatabaseConfig {\n /** The database client type (e.g. 'pg') */\n client: string;\n /** Whether the client type was overridden at the plugin level */\n clientOverridden: boolean;\n /** The optional role to set on connections */\n role: string | undefined;\n /** Additional knex configuration merged from base and plugin config */\n additionalKnexConfig: JsonObject | undefined;\n /** Whether to ensure the database exists */\n ensureExists: boolean;\n /** Whether to ensure the schema exists */\n ensureSchemaExists: boolean;\n /** The plugin division mode ('database' or 'schema') */\n pluginDivisionMode: string;\n /** The connection configuration */\n connection: Knex.PgConnectionConfig;\n /** The database name, if any */\n databaseName: string | undefined;\n /** The schema name, if using schema division mode */\n schemaName: string | undefined;\n /** Database client overrides including schema overrides if applicable */\n databaseClientOverrides: Knex.Config;\n /** The full knex config for the plugin */\n knexConfig: Knex.Config;\n}\n\n/**\n * Computes all postgres database configuration for a plugin from the provided config.\n *\n * @param config - The database config object\n * @param pluginId - The plugin ID to compute config for\n * @param prefix - The database name prefix (e.g. 'backstage_plugin_')\n * @returns All computed configuration values for the plugin\n */\nexport function computePgPluginConfig(\n config: Config,\n pluginId: string,\n prefix: string,\n schemaPrefix: string = '',\n): PgPluginDatabaseConfig {\n // Client type\n const pluginClient = config.getOptionalString(\n `${pluginPath(pluginId)}.client`,\n );\n const baseClient = config.getString('client');\n const client = pluginClient ?? baseClient;\n const clientOverridden = client !== baseClient;\n\n // Role\n const role =\n config.getOptionalString(`${pluginPath(pluginId)}.role`) ??\n config.getOptionalString('role');\n\n // Additional knex config\n const pluginKnexConfig = config\n .getOptionalConfig(`${pluginPath(pluginId)}.knexConfig`)\n ?.get<JsonObject>();\n const baseKnexConfig = config\n .getOptionalConfig('knexConfig')\n ?.get<JsonObject>();\n const additionalKnexConfig = merge(baseKnexConfig, pluginKnexConfig);\n\n // Ensure exists flags\n const baseEnsureExists = config.getOptionalBoolean('ensureExists') ?? true;\n const ensureExists =\n config.getOptionalBoolean(`${pluginPath(pluginId)}.ensureExists`) ??\n baseEnsureExists;\n\n const baseEnsureSchemaExists =\n config.getOptionalBoolean('ensureSchemaExists') ?? false;\n const ensureSchemaExists =\n config.getOptionalBoolean(\n `${pluginPath(pluginId)}.getEnsureSchemaExistsConfig`,\n ) ?? baseEnsureSchemaExists;\n\n // Plugin division mode\n const pluginDivisionMode =\n config.getOptionalString('pluginDivisionMode') ?? 'database';\n\n // Connection config\n let baseConnection = normalizeConnection(config.get('connection'));\n\n // Databases cannot be shared unless the `pluginDivisionMode` is set to `schema`.\n // The `database` property from the base connection is omitted unless\n // `pluginDivisionMode` is set to `schema`.\n if (pluginDivisionMode !== 'schema') {\n baseConnection = omit(baseConnection, 'database');\n }\n\n // Get and normalize optional plugin specific database connection\n const pluginConnection = normalizeConnection(\n config.getOptional(`${pluginPath(pluginId)}.connection`),\n );\n\n (\n baseConnection as Knex.PgConnectionConfig\n ).application_name ||= `backstage_plugin_${pluginId}`;\n\n const connection = {\n // Include base connection if client type has not been overridden\n ...(clientOverridden ? {} : baseConnection),\n ...pluginConnection,\n } as Knex.PgConnectionConfig;\n\n // Database name\n const connectionDatabaseName = (connection as Knex.ConnectionConfig)\n ?.database;\n let databaseName: string | undefined;\n\n if (pluginDivisionMode === 'schema') {\n // `pluginDivisionMode` as `schema` should use overridden databaseName if supplied\n // or fallback to default knex database\n databaseName = connectionDatabaseName;\n } else {\n // All other supported databases should fallback to an auto-prefixed name\n databaseName = connectionDatabaseName ?? `${prefix}${pluginId}`;\n }\n\n // Database client overrides\n let databaseClientOverrides: Knex.Config = {};\n let schemaName: string | undefined;\n\n if (databaseName) {\n databaseClientOverrides = { connection: { database: databaseName } };\n }\n if (pluginDivisionMode === 'schema') {\n schemaName = `${schemaPrefix}${pluginId}`;\n\n if (Buffer.byteLength(schemaName, 'utf8') > 63) {\n throw new Error(\n `PostgreSQL schema name \"${schemaName}\" exceeds the 63-byte limit. ` +\n `Consider using a shorter schemaPrefix (current: \"${schemaPrefix}\") or ` +\n `plugin ID (current: \"${pluginId}\").`,\n );\n }\n\n databaseClientOverrides = mergeDatabaseConfig({}, databaseClientOverrides, {\n searchPath: [schemaName],\n });\n }\n\n // Full knex config for plugin\n const knexConfig: Knex.Config = {\n ...additionalKnexConfig,\n client,\n connection,\n ...(role && { role }),\n };\n\n return {\n client,\n clientOverridden,\n role,\n additionalKnexConfig,\n ensureExists,\n ensureSchemaExists,\n pluginDivisionMode,\n connection,\n databaseName,\n schemaName,\n databaseClientOverrides,\n knexConfig,\n };\n}\n\nexport class PgConnector implements Connector {\n private readonly config: Config;\n private readonly prefix: string;\n private readonly schemaPrefix: string;\n private readonly databaseEnsureCache = new Map<string, Promise<void>>();\n private readonly customEnsureDatabaseExists?: typeof ensurePgDatabaseExists;\n private readonly databaseAdminPool: PgAdminPool;\n private readonly schemaAdminPool: PgAdminPool;\n\n constructor(\n config: Config,\n prefix: string,\n schemaPrefix: string,\n options: {\n ensureDatabaseExists?: typeof ensurePgDatabaseExists;\n createAdminClient?: (overrides: Knex.Config) => Promise<Knex>;\n adminPoolIdleTimeoutMillis?: number;\n } = {},\n ) {\n this.config = config;\n this.prefix = prefix;\n this.schemaPrefix = schemaPrefix;\n this.customEnsureDatabaseExists = options.ensureDatabaseExists;\n\n const createAdminClient =\n options.createAdminClient ??\n ((overrides: Knex.Config) =>\n createPgDatabaseClient(this.config, overrides));\n const idleTimeoutMillis = options.adminPoolIdleTimeoutMillis ?? 10_000;\n this.databaseAdminPool = new PgAdminPool(\n () => createAdminClient(PG_DATABASE_ADMIN_OVERRIDES),\n idleTimeoutMillis,\n );\n this.schemaAdminPool = new PgAdminPool(\n () => createAdminClient(PG_SCHEMA_ADMIN_OVERRIDES),\n idleTimeoutMillis,\n );\n }\n\n private async ensureDatabase(databaseName: string): Promise<void> {\n let promise = this.databaseEnsureCache.get(databaseName);\n if (!promise) {\n promise = this.customEnsureDatabaseExists\n ? this.customEnsureDatabaseExists(this.config, databaseName)\n : retryPgAdminOperation(() =>\n ddlLimiter(() =>\n this.runAdminOperation(this.databaseAdminPool, admin =>\n ensurePgDatabase(admin, databaseName),\n ),\n ),\n );\n this.databaseEnsureCache.set(databaseName, promise);\n }\n\n try {\n await promise;\n } catch (error) {\n if (this.databaseEnsureCache.get(databaseName) === promise) {\n this.databaseEnsureCache.delete(databaseName);\n }\n throw error;\n }\n }\n\n private async runAdminOperation<T>(\n pool: PgAdminPool,\n operation: (client: Knex) => Promise<T>,\n ): Promise<T> {\n try {\n return await pool.run(operation);\n } catch (error) {\n await pool.invalidate().catch(() => {});\n throw error;\n }\n }\n\n async shutdown(): Promise<void> {\n const results = await Promise.allSettled([\n this.databaseAdminPool.shutdown(),\n this.schemaAdminPool.shutdown(),\n ]);\n const errors = results\n .filter(\n (result): result is PromiseRejectedResult =>\n result.status === 'rejected',\n )\n .map(result => result.reason);\n if (errors.length === 1) {\n throw errors[0];\n }\n if (errors.length > 1) {\n throw new AggregateError(\n errors,\n 'Failed to shut down PostgreSQL admin pools',\n );\n }\n }\n\n async getClient(\n pluginId: string,\n _deps: {\n logger: LoggerService;\n lifecycle: LifecycleService;\n },\n ): Promise<Knex> {\n const pluginDbConfig = computePgPluginConfig(\n this.config,\n pluginId,\n this.prefix,\n this.schemaPrefix,\n );\n\n if (pluginDbConfig.databaseName && pluginDbConfig.ensureExists) {\n try {\n await this.ensureDatabase(pluginDbConfig.databaseName);\n } catch (error) {\n throw new Error(\n `Failed to connect to the database to make sure that '${pluginDbConfig.databaseName}' exists, ${error}`,\n );\n }\n }\n\n if (pluginDbConfig.pluginDivisionMode === 'schema') {\n if (pluginDbConfig.ensureSchemaExists || pluginDbConfig.ensureExists) {\n try {\n await ddlLimiter(() =>\n this.runAdminOperation(this.schemaAdminPool, admin =>\n ensurePgSchema(\n admin,\n this.config.getOptionalString('role'),\n pluginDbConfig.schemaName!,\n ),\n ),\n );\n } catch (error) {\n throw new Error(\n `Failed to connect to the database to make sure that schema for plugin '${pluginId}' exists, ${error}`,\n );\n }\n }\n }\n\n const client = createPgDatabaseClient(\n this.config,\n mergeDatabaseConfig(\n pluginDbConfig.knexConfig,\n pluginDbConfig.databaseClientOverrides,\n ),\n );\n\n return client;\n }\n}\n"],"names":["limiterFactory","knexFactory","format","config","mergeDatabaseConfig","ConfigReader","omit","durationToMilliseconds","readDurationFromConfig","ForwardedError","merge"],"mappings":"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"}
@@ -29,9 +29,9 @@ function getCookieFromRequest(req) {
29
29
  function willExpireSoon(expiresAt) {
30
30
  return Date.now() + FIVE_MINUTES_MS > expiresAt.getTime();
31
31
  }
32
- const credentialsSymbol = /* @__PURE__ */ Symbol("backstage-credentials");
33
- const limitedCredentialsSymbol = /* @__PURE__ */ Symbol("backstage-limited-credentials");
34
32
  class DefaultHttpAuthService {
33
+ #credentialsCache = /* @__PURE__ */ new WeakMap();
34
+ #limitedCredentialsCache = /* @__PURE__ */ new WeakMap();
35
35
  #auth;
36
36
  #discovery;
37
37
  #pluginId;
@@ -73,10 +73,20 @@ class DefaultHttpAuthService {
73
73
  return await this.#auth.getNoneCredentials();
74
74
  }
75
75
  async #getCredentials(req) {
76
- return req[credentialsSymbol] ??= this.#extractCredentialsFromRequest(req);
76
+ let credentials = this.#credentialsCache.get(req);
77
+ if (!credentials) {
78
+ credentials = this.#extractCredentialsFromRequest(req);
79
+ this.#credentialsCache.set(req, credentials);
80
+ }
81
+ return credentials;
77
82
  }
78
83
  async #getLimitedCredentials(req) {
79
- return req[limitedCredentialsSymbol] ??= this.#extractLimitedCredentialsFromRequest(req);
84
+ let credentials = this.#limitedCredentialsCache.get(req);
85
+ if (!credentials) {
86
+ credentials = this.#extractLimitedCredentialsFromRequest(req);
87
+ this.#limitedCredentialsCache.set(req, credentials);
88
+ }
89
+ return credentials;
80
90
  }
81
91
  async credentials(req, options) {
82
92
  const credentials = options?.allowLimitedAccess ? await this.#getLimitedCredentials(req) : await this.#getCredentials(req);
@@ -1 +1 @@
1
- {"version":3,"file":"httpAuthServiceFactory.cjs.js","sources":["../../../src/entrypoints/httpAuth/httpAuthServiceFactory.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 AuthService,\n BackstageCredentials,\n BackstagePrincipalTypes,\n BackstageUserPrincipal,\n DiscoveryService,\n HttpAuthService,\n coreServices,\n createServiceFactory,\n} from '@backstage/backend-plugin-api';\nimport { AuthenticationError, NotAllowedError } from '@backstage/errors';\nimport { parse as parseCookie } from 'cookie';\nimport { Request, Response } from 'express';\n\nconst FIVE_MINUTES_MS = 5 * 60 * 1000;\n\nconst BACKSTAGE_AUTH_COOKIE = 'backstage-auth';\n\nfunction getTokenFromRequest(req: Request) {\n let token: string | undefined;\n const authHeader = req.headers.authorization;\n if (typeof authHeader === 'string') {\n const matches = authHeader.match(/^Bearer[ ]+(\\S+)$/i);\n token = matches?.[1];\n }\n\n return { token };\n}\n\nfunction getCookieFromRequest(req: Request) {\n const cookieHeader = req.headers.cookie;\n if (cookieHeader) {\n const cookies = parseCookie(cookieHeader);\n const token = cookies[BACKSTAGE_AUTH_COOKIE];\n if (token) {\n return token;\n }\n }\n\n return undefined;\n}\n\nfunction willExpireSoon(expiresAt: Date) {\n return Date.now() + FIVE_MINUTES_MS > expiresAt.getTime();\n}\n\nconst credentialsSymbol = Symbol('backstage-credentials');\nconst limitedCredentialsSymbol = Symbol('backstage-limited-credentials');\n\ntype RequestWithCredentials = Request & {\n [credentialsSymbol]?: Promise<BackstageCredentials>;\n [limitedCredentialsSymbol]?: Promise<BackstageCredentials>;\n};\n\n/**\n * @public\n * Options for creating a DefaultHttpAuthService.\n */\nexport interface DefaultHttpAuthServiceOptions {\n auth: AuthService;\n discovery: DiscoveryService;\n pluginId: string;\n /**\n * Optionally override logic for extracting the token from the request.\n */\n getTokenFromRequest?: (req: Request) => { token?: string };\n}\n\n/**\n * @public\n * DefaultHttpAuthService is the default implementation of the HttpAuthService\n */\nexport class DefaultHttpAuthService implements HttpAuthService {\n readonly #auth: AuthService;\n readonly #discovery: DiscoveryService;\n readonly #pluginId: string;\n readonly #getToken: (req: Request) => { token?: string };\n\n private constructor(\n auth: AuthService,\n discovery: DiscoveryService,\n pluginId: string,\n getToken?: (req: Request) => { token?: string },\n ) {\n this.#auth = auth;\n this.#discovery = discovery;\n this.#pluginId = pluginId;\n this.#getToken = getToken ?? getTokenFromRequest;\n }\n\n static create(\n options: DefaultHttpAuthServiceOptions,\n ): DefaultHttpAuthService {\n return new DefaultHttpAuthService(\n options.auth,\n options.discovery,\n options.pluginId,\n options.getTokenFromRequest,\n );\n }\n\n async #extractCredentialsFromRequest(req: Request) {\n const { token } = this.#getToken(req);\n if (!token) {\n return await this.#auth.getNoneCredentials();\n }\n\n return await this.#auth.authenticate(token);\n }\n\n async #extractLimitedCredentialsFromRequest(req: Request) {\n const { token } = this.#getToken(req);\n if (token) {\n return await this.#auth.authenticate(token, {\n allowLimitedAccess: true,\n });\n }\n\n const cookie = getCookieFromRequest(req);\n if (cookie) {\n return await this.#auth.authenticate(cookie, {\n allowLimitedAccess: true,\n });\n }\n\n return await this.#auth.getNoneCredentials();\n }\n\n async #getCredentials(req: RequestWithCredentials) {\n return (req[credentialsSymbol] ??=\n this.#extractCredentialsFromRequest(req));\n }\n\n async #getLimitedCredentials(req: RequestWithCredentials) {\n return (req[limitedCredentialsSymbol] ??=\n this.#extractLimitedCredentialsFromRequest(req));\n }\n\n async credentials<TAllowed extends keyof BackstagePrincipalTypes = 'unknown'>(\n req: Request,\n options?: {\n allow?: Array<TAllowed>;\n allowLimitedAccess?: boolean;\n },\n ): Promise<BackstageCredentials<BackstagePrincipalTypes[TAllowed]>> {\n // Limited and full credentials are treated as two separate cases, this lets\n // us avoid internal dependencies between the AuthService and\n // HttpAuthService implementations\n const credentials = options?.allowLimitedAccess\n ? await this.#getLimitedCredentials(req)\n : await this.#getCredentials(req);\n\n const allowed = options?.allow;\n if (!allowed) {\n return credentials as any;\n }\n\n if (this.#auth.isPrincipal(credentials, 'none')) {\n if (allowed.includes('none' as TAllowed)) {\n return credentials as any;\n }\n\n throw new AuthenticationError('Missing credentials');\n } else if (this.#auth.isPrincipal(credentials, 'user')) {\n if (allowed.includes('user' as TAllowed)) {\n return credentials as any;\n }\n\n throw new NotAllowedError(\n `This endpoint does not allow 'user' credentials`,\n );\n } else if (this.#auth.isPrincipal(credentials, 'service')) {\n if (allowed.includes('service' as TAllowed)) {\n return credentials as any;\n }\n\n throw new NotAllowedError(\n `This endpoint does not allow 'service' credentials`,\n );\n }\n\n throw new NotAllowedError(\n 'Unknown principal type, this should never happen',\n );\n }\n\n async issueUserCookie(\n res: Response,\n options?: { credentials?: BackstageCredentials },\n ): Promise<{ expiresAt: Date }> {\n if (res.headersSent) {\n throw new Error('Failed to issue user cookie, headers were already sent');\n }\n\n let credentials: BackstageCredentials<BackstageUserPrincipal>;\n if (options?.credentials) {\n if (this.#auth.isPrincipal(options.credentials, 'none')) {\n res.clearCookie(\n BACKSTAGE_AUTH_COOKIE,\n await this.#getCookieOptions(res.req),\n );\n return { expiresAt: new Date() };\n }\n if (!this.#auth.isPrincipal(options.credentials, 'user')) {\n throw new AuthenticationError(\n 'Refused to issue cookie for non-user principal',\n );\n }\n credentials = options.credentials;\n } else {\n credentials = await this.credentials(res.req, { allow: ['user'] });\n }\n\n const existingExpiresAt = await this.#existingCookieExpiration(res.req);\n if (existingExpiresAt && !willExpireSoon(existingExpiresAt)) {\n return { expiresAt: existingExpiresAt };\n }\n\n const { token, expiresAt } = await this.#auth.getLimitedUserToken(\n credentials,\n );\n if (!token) {\n throw new Error('User credentials is unexpectedly missing token');\n }\n\n res.cookie(BACKSTAGE_AUTH_COOKIE, token, {\n ...(await this.#getCookieOptions(res.req)),\n expires: expiresAt,\n });\n\n return { expiresAt };\n }\n\n async #getCookieOptions(_req: Request): Promise<{\n domain: string;\n httpOnly: true;\n secure: boolean;\n priority: 'high';\n sameSite: 'none' | 'lax';\n }> {\n // TODO: eventually we should read from `${req.protocol}://${req.hostname}`\n // once https://github.com/backstage/backstage/issues/24169 has landed\n const externalBaseUrlStr = await this.#discovery.getExternalBaseUrl(\n this.#pluginId,\n );\n const externalBaseUrl = new URL(externalBaseUrlStr);\n\n const secure =\n externalBaseUrl.protocol === 'https:' ||\n externalBaseUrl.hostname === 'localhost';\n\n return {\n domain: externalBaseUrl.hostname,\n httpOnly: true,\n secure,\n priority: 'high',\n sameSite: secure ? 'none' : 'lax',\n };\n }\n\n async #existingCookieExpiration(req: Request): Promise<Date | undefined> {\n const existingCookie = getCookieFromRequest(req);\n if (!existingCookie) {\n return undefined;\n }\n\n try {\n const existingCredentials = await this.#auth.authenticate(\n existingCookie,\n {\n allowLimitedAccess: true,\n },\n );\n if (!this.#auth.isPrincipal(existingCredentials, 'user')) {\n return undefined;\n }\n\n return existingCredentials.expiresAt;\n } catch (error) {\n if (error.name === 'AuthenticationError') {\n return undefined;\n }\n throw error;\n }\n }\n}\n\n/**\n * Authentication of HTTP requests.\n *\n * See {@link @backstage/code-plugin-api#HttpAuthService}\n * and {@link https://backstage.io/docs/backend-system/core-services/http-auth | the service docs}\n * for more information.\n *\n * @public\n */\nexport const httpAuthServiceFactory = createServiceFactory({\n service: coreServices.httpAuth,\n deps: {\n auth: coreServices.auth,\n discovery: coreServices.discovery,\n plugin: coreServices.pluginMetadata,\n },\n async factory({ auth, discovery, plugin }) {\n return DefaultHttpAuthService.create({\n auth,\n discovery,\n pluginId: plugin.getId(),\n });\n },\n});\n"],"names":["parseCookie","AuthenticationError","NotAllowedError","createServiceFactory","coreServices"],"mappings":";;;;;;AA8BA,MAAM,eAAA,GAAkB,IAAI,EAAA,GAAK,GAAA;AAEjC,MAAM,qBAAA,GAAwB,gBAAA;AAE9B,SAAS,oBAAoB,GAAA,EAAc;AACzC,EAAA,IAAI,KAAA;AACJ,EAAA,MAAM,UAAA,GAAa,IAAI,OAAA,CAAQ,aAAA;AAC/B,EAAA,IAAI,OAAO,eAAe,QAAA,EAAU;AAClC,IAAA,MAAM,OAAA,GAAU,UAAA,CAAW,KAAA,CAAM,oBAAoB,CAAA;AACrD,IAAA,KAAA,GAAQ,UAAU,CAAC,CAAA;AAAA,EACrB;AAEA,EAAA,OAAO,EAAE,KAAA,EAAM;AACjB;AAEA,SAAS,qBAAqB,GAAA,EAAc;AAC1C,EAAA,MAAM,YAAA,GAAe,IAAI,OAAA,CAAQ,MAAA;AACjC,EAAA,IAAI,YAAA,EAAc;AAChB,IAAA,MAAM,OAAA,GAAUA,aAAY,YAAY,CAAA;AACxC,IAAA,MAAM,KAAA,GAAQ,QAAQ,qBAAqB,CAAA;AAC3C,IAAA,IAAI,KAAA,EAAO;AACT,MAAA,OAAO,KAAA;AAAA,IACT;AAAA,EACF;AAEA,EAAA,OAAO,MAAA;AACT;AAEA,SAAS,eAAe,SAAA,EAAiB;AACvC,EAAA,OAAO,IAAA,CAAK,GAAA,EAAI,GAAI,eAAA,GAAkB,UAAU,OAAA,EAAQ;AAC1D;AAEA,MAAM,iBAAA,0BAA2B,uBAAuB,CAAA;AACxD,MAAM,wBAAA,0BAAkC,+BAA+B,CAAA;AAyBhE,MAAM,sBAAA,CAAkD;AAAA,EACpD,KAAA;AAAA,EACA,UAAA;AAAA,EACA,SAAA;AAAA,EACA,SAAA;AAAA,EAED,WAAA,CACN,IAAA,EACA,SAAA,EACA,QAAA,EACA,QAAA,EACA;AACA,IAAA,IAAA,CAAK,KAAA,GAAQ,IAAA;AACb,IAAA,IAAA,CAAK,UAAA,GAAa,SAAA;AAClB,IAAA,IAAA,CAAK,SAAA,GAAY,QAAA;AACjB,IAAA,IAAA,CAAK,YAAY,QAAA,IAAY,mBAAA;AAAA,EAC/B;AAAA,EAEA,OAAO,OACL,OAAA,EACwB;AACxB,IAAA,OAAO,IAAI,sBAAA;AAAA,MACT,OAAA,CAAQ,IAAA;AAAA,MACR,OAAA,CAAQ,SAAA;AAAA,MACR,OAAA,CAAQ,QAAA;AAAA,MACR,OAAA,CAAQ;AAAA,KACV;AAAA,EACF;AAAA,EAEA,MAAM,+BAA+B,GAAA,EAAc;AACjD,IAAA,MAAM,EAAE,KAAA,EAAM,GAAI,IAAA,CAAK,UAAU,GAAG,CAAA;AACpC,IAAA,IAAI,CAAC,KAAA,EAAO;AACV,MAAA,OAAO,MAAM,IAAA,CAAK,KAAA,CAAM,kBAAA,EAAmB;AAAA,IAC7C;AAEA,IAAA,OAAO,MAAM,IAAA,CAAK,KAAA,CAAM,YAAA,CAAa,KAAK,CAAA;AAAA,EAC5C;AAAA,EAEA,MAAM,sCAAsC,GAAA,EAAc;AACxD,IAAA,MAAM,EAAE,KAAA,EAAM,GAAI,IAAA,CAAK,UAAU,GAAG,CAAA;AACpC,IAAA,IAAI,KAAA,EAAO;AACT,MAAA,OAAO,MAAM,IAAA,CAAK,KAAA,CAAM,YAAA,CAAa,KAAA,EAAO;AAAA,QAC1C,kBAAA,EAAoB;AAAA,OACrB,CAAA;AAAA,IACH;AAEA,IAAA,MAAM,MAAA,GAAS,qBAAqB,GAAG,CAAA;AACvC,IAAA,IAAI,MAAA,EAAQ;AACV,MAAA,OAAO,MAAM,IAAA,CAAK,KAAA,CAAM,YAAA,CAAa,MAAA,EAAQ;AAAA,QAC3C,kBAAA,EAAoB;AAAA,OACrB,CAAA;AAAA,IACH;AAEA,IAAA,OAAO,MAAM,IAAA,CAAK,KAAA,CAAM,kBAAA,EAAmB;AAAA,EAC7C;AAAA,EAEA,MAAM,gBAAgB,GAAA,EAA6B;AACjD,IAAA,OAAQ,GAAA,CAAI,iBAAiB,CAAA,KAC3B,IAAA,CAAK,+BAA+B,GAAG,CAAA;AAAA,EAC3C;AAAA,EAEA,MAAM,uBAAuB,GAAA,EAA6B;AACxD,IAAA,OAAQ,GAAA,CAAI,wBAAwB,CAAA,KAClC,IAAA,CAAK,sCAAsC,GAAG,CAAA;AAAA,EAClD;AAAA,EAEA,MAAM,WAAA,CACJ,GAAA,EACA,OAAA,EAIkE;AAIlE,IAAA,MAAM,WAAA,GAAc,OAAA,EAAS,kBAAA,GACzB,MAAM,IAAA,CAAK,sBAAA,CAAuB,GAAG,CAAA,GACrC,MAAM,IAAA,CAAK,eAAA,CAAgB,GAAG,CAAA;AAElC,IAAA,MAAM,UAAU,OAAA,EAAS,KAAA;AACzB,IAAA,IAAI,CAAC,OAAA,EAAS;AACZ,MAAA,OAAO,WAAA;AAAA,IACT;AAEA,IAAA,IAAI,IAAA,CAAK,KAAA,CAAM,WAAA,CAAY,WAAA,EAAa,MAAM,CAAA,EAAG;AAC/C,MAAA,IAAI,OAAA,CAAQ,QAAA,CAAS,MAAkB,CAAA,EAAG;AACxC,QAAA,OAAO,WAAA;AAAA,MACT;AAEA,MAAA,MAAM,IAAIC,2BAAoB,qBAAqB,CAAA;AAAA,IACrD,WAAW,IAAA,CAAK,KAAA,CAAM,WAAA,CAAY,WAAA,EAAa,MAAM,CAAA,EAAG;AACtD,MAAA,IAAI,OAAA,CAAQ,QAAA,CAAS,MAAkB,CAAA,EAAG;AACxC,QAAA,OAAO,WAAA;AAAA,MACT;AAEA,MAAA,MAAM,IAAIC,sBAAA;AAAA,QACR,CAAA,+CAAA;AAAA,OACF;AAAA,IACF,WAAW,IAAA,CAAK,KAAA,CAAM,WAAA,CAAY,WAAA,EAAa,SAAS,CAAA,EAAG;AACzD,MAAA,IAAI,OAAA,CAAQ,QAAA,CAAS,SAAqB,CAAA,EAAG;AAC3C,QAAA,OAAO,WAAA;AAAA,MACT;AAEA,MAAA,MAAM,IAAIA,sBAAA;AAAA,QACR,CAAA,kDAAA;AAAA,OACF;AAAA,IACF;AAEA,IAAA,MAAM,IAAIA,sBAAA;AAAA,MACR;AAAA,KACF;AAAA,EACF;AAAA,EAEA,MAAM,eAAA,CACJ,GAAA,EACA,OAAA,EAC8B;AAC9B,IAAA,IAAI,IAAI,WAAA,EAAa;AACnB,MAAA,MAAM,IAAI,MAAM,wDAAwD,CAAA;AAAA,IAC1E;AAEA,IAAA,IAAI,WAAA;AACJ,IAAA,IAAI,SAAS,WAAA,EAAa;AACxB,MAAA,IAAI,KAAK,KAAA,CAAM,WAAA,CAAY,OAAA,CAAQ,WAAA,EAAa,MAAM,CAAA,EAAG;AACvD,QAAA,GAAA,CAAI,WAAA;AAAA,UACF,qBAAA;AAAA,UACA,MAAM,IAAA,CAAK,iBAAA,CAAkB,GAAA,CAAI,GAAG;AAAA,SACtC;AACA,QAAA,OAAO,EAAE,SAAA,kBAAW,IAAI,IAAA,EAAK,EAAE;AAAA,MACjC;AACA,MAAA,IAAI,CAAC,IAAA,CAAK,KAAA,CAAM,YAAY,OAAA,CAAQ,WAAA,EAAa,MAAM,CAAA,EAAG;AACxD,QAAA,MAAM,IAAID,0BAAA;AAAA,UACR;AAAA,SACF;AAAA,MACF;AACA,MAAA,WAAA,GAAc,OAAA,CAAQ,WAAA;AAAA,IACxB,CAAA,MAAO;AACL,MAAA,WAAA,GAAc,MAAM,IAAA,CAAK,WAAA,CAAY,GAAA,CAAI,GAAA,EAAK,EAAE,KAAA,EAAO,CAAC,MAAM,CAAA,EAAG,CAAA;AAAA,IACnE;AAEA,IAAA,MAAM,iBAAA,GAAoB,MAAM,IAAA,CAAK,yBAAA,CAA0B,IAAI,GAAG,CAAA;AACtE,IAAA,IAAI,iBAAA,IAAqB,CAAC,cAAA,CAAe,iBAAiB,CAAA,EAAG;AAC3D,MAAA,OAAO,EAAE,WAAW,iBAAA,EAAkB;AAAA,IACxC;AAEA,IAAA,MAAM,EAAE,KAAA,EAAO,SAAA,EAAU,GAAI,MAAM,KAAK,KAAA,CAAM,mBAAA;AAAA,MAC5C;AAAA,KACF;AACA,IAAA,IAAI,CAAC,KAAA,EAAO;AACV,MAAA,MAAM,IAAI,MAAM,gDAAgD,CAAA;AAAA,IAClE;AAEA,IAAA,GAAA,CAAI,MAAA,CAAO,uBAAuB,KAAA,EAAO;AAAA,MACvC,GAAI,MAAM,IAAA,CAAK,iBAAA,CAAkB,IAAI,GAAG,CAAA;AAAA,MACxC,OAAA,EAAS;AAAA,KACV,CAAA;AAED,IAAA,OAAO,EAAE,SAAA,EAAU;AAAA,EACrB;AAAA,EAEA,MAAM,kBAAkB,IAAA,EAMrB;AAGD,IAAA,MAAM,kBAAA,GAAqB,MAAM,IAAA,CAAK,UAAA,CAAW,kBAAA;AAAA,MAC/C,IAAA,CAAK;AAAA,KACP;AACA,IAAA,MAAM,eAAA,GAAkB,IAAI,GAAA,CAAI,kBAAkB,CAAA;AAElD,IAAA,MAAM,MAAA,GACJ,eAAA,CAAgB,QAAA,KAAa,QAAA,IAC7B,gBAAgB,QAAA,KAAa,WAAA;AAE/B,IAAA,OAAO;AAAA,MACL,QAAQ,eAAA,CAAgB,QAAA;AAAA,MACxB,QAAA,EAAU,IAAA;AAAA,MACV,MAAA;AAAA,MACA,QAAA,EAAU,MAAA;AAAA,MACV,QAAA,EAAU,SAAS,MAAA,GAAS;AAAA,KAC9B;AAAA,EACF;AAAA,EAEA,MAAM,0BAA0B,GAAA,EAAyC;AACvE,IAAA,MAAM,cAAA,GAAiB,qBAAqB,GAAG,CAAA;AAC/C,IAAA,IAAI,CAAC,cAAA,EAAgB;AACnB,MAAA,OAAO,MAAA;AAAA,IACT;AAEA,IAAA,IAAI;AACF,MAAA,MAAM,mBAAA,GAAsB,MAAM,IAAA,CAAK,KAAA,CAAM,YAAA;AAAA,QAC3C,cAAA;AAAA,QACA;AAAA,UACE,kBAAA,EAAoB;AAAA;AACtB,OACF;AACA,MAAA,IAAI,CAAC,IAAA,CAAK,KAAA,CAAM,WAAA,CAAY,mBAAA,EAAqB,MAAM,CAAA,EAAG;AACxD,QAAA,OAAO,KAAA,CAAA;AAAA,MACT;AAEA,MAAA,OAAO,mBAAA,CAAoB,SAAA;AAAA,IAC7B,SAAS,KAAA,EAAO;AACd,MAAA,IAAI,KAAA,CAAM,SAAS,qBAAA,EAAuB;AACxC,QAAA,OAAO,MAAA;AAAA,MACT;AACA,MAAA,MAAM,KAAA;AAAA,IACR;AAAA,EACF;AACF;AAWO,MAAM,yBAAyBE,qCAAA,CAAqB;AAAA,EACzD,SAASC,6BAAA,CAAa,QAAA;AAAA,EACtB,IAAA,EAAM;AAAA,IACJ,MAAMA,6BAAA,CAAa,IAAA;AAAA,IACnB,WAAWA,6BAAA,CAAa,SAAA;AAAA,IACxB,QAAQA,6BAAA,CAAa;AAAA,GACvB;AAAA,EACA,MAAM,OAAA,CAAQ,EAAE,IAAA,EAAM,SAAA,EAAW,QAAO,EAAG;AACzC,IAAA,OAAO,uBAAuB,MAAA,CAAO;AAAA,MACnC,IAAA;AAAA,MACA,SAAA;AAAA,MACA,QAAA,EAAU,OAAO,KAAA;AAAM,KACxB,CAAA;AAAA,EACH;AACF,CAAC;;;;;"}
1
+ {"version":3,"file":"httpAuthServiceFactory.cjs.js","sources":["../../../src/entrypoints/httpAuth/httpAuthServiceFactory.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 AuthService,\n BackstageCredentials,\n BackstagePrincipalTypes,\n BackstageUserPrincipal,\n DiscoveryService,\n HttpAuthService,\n coreServices,\n createServiceFactory,\n} from '@backstage/backend-plugin-api';\nimport { AuthenticationError, NotAllowedError } from '@backstage/errors';\nimport { parse as parseCookie } from 'cookie';\nimport { Request, Response } from 'express';\n\nconst FIVE_MINUTES_MS = 5 * 60 * 1000;\n\nconst BACKSTAGE_AUTH_COOKIE = 'backstage-auth';\n\nfunction getTokenFromRequest(req: Request) {\n let token: string | undefined;\n const authHeader = req.headers.authorization;\n if (typeof authHeader === 'string') {\n const matches = authHeader.match(/^Bearer[ ]+(\\S+)$/i);\n token = matches?.[1];\n }\n\n return { token };\n}\n\nfunction getCookieFromRequest(req: Request) {\n const cookieHeader = req.headers.cookie;\n if (cookieHeader) {\n const cookies = parseCookie(cookieHeader);\n const token = cookies[BACKSTAGE_AUTH_COOKIE];\n if (token) {\n return token;\n }\n }\n\n return undefined;\n}\n\nfunction willExpireSoon(expiresAt: Date) {\n return Date.now() + FIVE_MINUTES_MS > expiresAt.getTime();\n}\n\n/**\n * @public\n * Options for creating a DefaultHttpAuthService.\n */\nexport interface DefaultHttpAuthServiceOptions {\n auth: AuthService;\n discovery: DiscoveryService;\n pluginId: string;\n /**\n * Optionally override logic for extracting the token from the request.\n */\n getTokenFromRequest?: (req: Request) => { token?: string };\n}\n\n/**\n * @public\n * DefaultHttpAuthService is the default implementation of the HttpAuthService\n */\nexport class DefaultHttpAuthService implements HttpAuthService {\n readonly #credentialsCache = new WeakMap<\n Request,\n Promise<BackstageCredentials>\n >();\n readonly #limitedCredentialsCache = new WeakMap<\n Request,\n Promise<BackstageCredentials>\n >();\n readonly #auth: AuthService;\n readonly #discovery: DiscoveryService;\n readonly #pluginId: string;\n readonly #getToken: (req: Request) => { token?: string };\n\n private constructor(\n auth: AuthService,\n discovery: DiscoveryService,\n pluginId: string,\n getToken?: (req: Request) => { token?: string },\n ) {\n this.#auth = auth;\n this.#discovery = discovery;\n this.#pluginId = pluginId;\n this.#getToken = getToken ?? getTokenFromRequest;\n }\n\n static create(\n options: DefaultHttpAuthServiceOptions,\n ): DefaultHttpAuthService {\n return new DefaultHttpAuthService(\n options.auth,\n options.discovery,\n options.pluginId,\n options.getTokenFromRequest,\n );\n }\n\n async #extractCredentialsFromRequest(req: Request) {\n const { token } = this.#getToken(req);\n if (!token) {\n return await this.#auth.getNoneCredentials();\n }\n\n return await this.#auth.authenticate(token);\n }\n\n async #extractLimitedCredentialsFromRequest(req: Request) {\n const { token } = this.#getToken(req);\n if (token) {\n return await this.#auth.authenticate(token, {\n allowLimitedAccess: true,\n });\n }\n\n const cookie = getCookieFromRequest(req);\n if (cookie) {\n return await this.#auth.authenticate(cookie, {\n allowLimitedAccess: true,\n });\n }\n\n return await this.#auth.getNoneCredentials();\n }\n\n async #getCredentials(req: Request) {\n let credentials = this.#credentialsCache.get(req);\n if (!credentials) {\n credentials = this.#extractCredentialsFromRequest(req);\n this.#credentialsCache.set(req, credentials);\n }\n return credentials;\n }\n\n async #getLimitedCredentials(req: Request) {\n let credentials = this.#limitedCredentialsCache.get(req);\n if (!credentials) {\n credentials = this.#extractLimitedCredentialsFromRequest(req);\n this.#limitedCredentialsCache.set(req, credentials);\n }\n return credentials;\n }\n\n async credentials<TAllowed extends keyof BackstagePrincipalTypes = 'unknown'>(\n req: Request,\n options?: {\n allow?: Array<TAllowed>;\n allowLimitedAccess?: boolean;\n },\n ): Promise<BackstageCredentials<BackstagePrincipalTypes[TAllowed]>> {\n // Limited and full credentials are treated as two separate cases, this lets\n // us avoid internal dependencies between the AuthService and\n // HttpAuthService implementations\n const credentials = options?.allowLimitedAccess\n ? await this.#getLimitedCredentials(req)\n : await this.#getCredentials(req);\n\n const allowed = options?.allow;\n if (!allowed) {\n return credentials as any;\n }\n\n if (this.#auth.isPrincipal(credentials, 'none')) {\n if (allowed.includes('none' as TAllowed)) {\n return credentials as any;\n }\n\n throw new AuthenticationError('Missing credentials');\n } else if (this.#auth.isPrincipal(credentials, 'user')) {\n if (allowed.includes('user' as TAllowed)) {\n return credentials as any;\n }\n\n throw new NotAllowedError(\n `This endpoint does not allow 'user' credentials`,\n );\n } else if (this.#auth.isPrincipal(credentials, 'service')) {\n if (allowed.includes('service' as TAllowed)) {\n return credentials as any;\n }\n\n throw new NotAllowedError(\n `This endpoint does not allow 'service' credentials`,\n );\n }\n\n throw new NotAllowedError(\n 'Unknown principal type, this should never happen',\n );\n }\n\n async issueUserCookie(\n res: Response,\n options?: { credentials?: BackstageCredentials },\n ): Promise<{ expiresAt: Date }> {\n if (res.headersSent) {\n throw new Error('Failed to issue user cookie, headers were already sent');\n }\n\n let credentials: BackstageCredentials<BackstageUserPrincipal>;\n if (options?.credentials) {\n if (this.#auth.isPrincipal(options.credentials, 'none')) {\n res.clearCookie(\n BACKSTAGE_AUTH_COOKIE,\n await this.#getCookieOptions(res.req),\n );\n return { expiresAt: new Date() };\n }\n if (!this.#auth.isPrincipal(options.credentials, 'user')) {\n throw new AuthenticationError(\n 'Refused to issue cookie for non-user principal',\n );\n }\n credentials = options.credentials;\n } else {\n credentials = await this.credentials(res.req, { allow: ['user'] });\n }\n\n const existingExpiresAt = await this.#existingCookieExpiration(res.req);\n if (existingExpiresAt && !willExpireSoon(existingExpiresAt)) {\n return { expiresAt: existingExpiresAt };\n }\n\n const { token, expiresAt } = await this.#auth.getLimitedUserToken(\n credentials,\n );\n if (!token) {\n throw new Error('User credentials is unexpectedly missing token');\n }\n\n res.cookie(BACKSTAGE_AUTH_COOKIE, token, {\n ...(await this.#getCookieOptions(res.req)),\n expires: expiresAt,\n });\n\n return { expiresAt };\n }\n\n async #getCookieOptions(_req: Request): Promise<{\n domain: string;\n httpOnly: true;\n secure: boolean;\n priority: 'high';\n sameSite: 'none' | 'lax';\n }> {\n // TODO: eventually we should read from `${req.protocol}://${req.hostname}`\n // once https://github.com/backstage/backstage/issues/24169 has landed\n const externalBaseUrlStr = await this.#discovery.getExternalBaseUrl(\n this.#pluginId,\n );\n const externalBaseUrl = new URL(externalBaseUrlStr);\n\n const secure =\n externalBaseUrl.protocol === 'https:' ||\n externalBaseUrl.hostname === 'localhost';\n\n return {\n domain: externalBaseUrl.hostname,\n httpOnly: true,\n secure,\n priority: 'high',\n sameSite: secure ? 'none' : 'lax',\n };\n }\n\n async #existingCookieExpiration(req: Request): Promise<Date | undefined> {\n const existingCookie = getCookieFromRequest(req);\n if (!existingCookie) {\n return undefined;\n }\n\n try {\n const existingCredentials = await this.#auth.authenticate(\n existingCookie,\n {\n allowLimitedAccess: true,\n },\n );\n if (!this.#auth.isPrincipal(existingCredentials, 'user')) {\n return undefined;\n }\n\n return existingCredentials.expiresAt;\n } catch (error) {\n if (error.name === 'AuthenticationError') {\n return undefined;\n }\n throw error;\n }\n }\n}\n\n/**\n * Authentication of HTTP requests.\n *\n * See {@link @backstage/code-plugin-api#HttpAuthService}\n * and {@link https://backstage.io/docs/backend-system/core-services/http-auth | the service docs}\n * for more information.\n *\n * @public\n */\nexport const httpAuthServiceFactory = createServiceFactory({\n service: coreServices.httpAuth,\n deps: {\n auth: coreServices.auth,\n discovery: coreServices.discovery,\n plugin: coreServices.pluginMetadata,\n },\n async factory({ auth, discovery, plugin }) {\n return DefaultHttpAuthService.create({\n auth,\n discovery,\n pluginId: plugin.getId(),\n });\n },\n});\n"],"names":["parseCookie","AuthenticationError","NotAllowedError","createServiceFactory","coreServices"],"mappings":"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"}
@@ -8,7 +8,7 @@ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'defau
8
8
  var trimEnd__default = /*#__PURE__*/_interopDefaultCompat(trimEnd);
9
9
 
10
10
  function normalizePath(path) {
11
- return `${trimEnd__default.default(path, "/")}/`;
11
+ return `${trimEnd__default.default(path, "/")}/`.toLowerCase();
12
12
  }
13
13
  class DefaultRootHttpRouter {
14
14
  #indexPath;
@@ -1 +1 @@
1
- {"version":3,"file":"DefaultRootHttpRouter.cjs.js","sources":["../../../src/entrypoints/rootHttpRouter/DefaultRootHttpRouter.ts"],"sourcesContent":["/*\n * Copyright 2023 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 { RootHttpRouterService } from '@backstage/backend-plugin-api';\nimport { Handler, Router } from 'express';\nimport trimEnd from 'lodash/trimEnd';\n\nfunction normalizePath(path: string): string {\n return `${trimEnd(path, '/')}/`;\n}\n\n/**\n * Options for the {@link DefaultRootHttpRouter} class.\n *\n * @public\n */\nexport interface DefaultRootHttpRouterOptions {\n /**\n * The path to forward all unmatched requests to. Defaults to '/api/app' if\n * not given. Disables index path behavior if false is given.\n */\n indexPath?: string | false;\n}\n\n/**\n * The default implementation of the {@link @backstage/backend-plugin-api#RootHttpRouterService} interface for\n * {@link @backstage/backend-plugin-api#coreServices.rootHttpRouter}.\n *\n * @public\n */\nexport class DefaultRootHttpRouter implements RootHttpRouterService {\n #indexPath?: string;\n\n #router = Router();\n #namedRoutes = Router();\n #indexRouter = Router();\n #existingPaths = new Array<string>();\n\n static create(options?: DefaultRootHttpRouterOptions) {\n let indexPath;\n if (options?.indexPath === false) {\n indexPath = undefined;\n } else if (options?.indexPath === undefined) {\n indexPath = '/api/app';\n } else if (options?.indexPath === '') {\n throw new Error('indexPath option may not be an empty string');\n } else {\n indexPath = options.indexPath;\n }\n return new DefaultRootHttpRouter(indexPath);\n }\n\n private constructor(indexPath?: string) {\n this.#indexPath = indexPath;\n this.#router.use(this.#namedRoutes);\n\n // Any request with a /api/ prefix will skip the index router, even if no named router matches\n this.#router.use('/api/', (_req, _res, next) => {\n next('router');\n });\n\n if (this.#indexPath) {\n this.#router.use(this.#indexRouter);\n }\n }\n\n use(path: string, handler: Handler) {\n if (path.match(/^[/\\s]*$/)) {\n throw new Error(`Root router path may not be empty`);\n }\n const conflictingPath = this.#findConflictingPath(path);\n if (conflictingPath) {\n throw new Error(\n `Path ${path} conflicts with the existing path ${conflictingPath}`,\n );\n }\n this.#existingPaths.push(path);\n this.#namedRoutes.use(path, handler);\n\n if (this.#indexPath === path) {\n this.#indexRouter.use(handler);\n }\n }\n\n handler(): Handler {\n return this.#router;\n }\n\n #findConflictingPath(newPath: string): string | undefined {\n const normalizedNewPath = normalizePath(newPath);\n for (const path of this.#existingPaths) {\n const normalizedPath = normalizePath(path);\n if (normalizedPath.startsWith(normalizedNewPath)) {\n return path;\n }\n if (normalizedNewPath.startsWith(normalizedPath)) {\n return path;\n }\n }\n return undefined;\n }\n}\n"],"names":["trimEnd","Router"],"mappings":"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"}
1
+ {"version":3,"file":"DefaultRootHttpRouter.cjs.js","sources":["../../../src/entrypoints/rootHttpRouter/DefaultRootHttpRouter.ts"],"sourcesContent":["/*\n * Copyright 2023 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 { RootHttpRouterService } from '@backstage/backend-plugin-api';\nimport { Handler, Router } from 'express';\nimport trimEnd from 'lodash/trimEnd';\n\nfunction normalizePath(path: string): string {\n return `${trimEnd(path, '/')}/`.toLowerCase();\n}\n\n/**\n * Options for the {@link DefaultRootHttpRouter} class.\n *\n * @public\n */\nexport interface DefaultRootHttpRouterOptions {\n /**\n * The path to forward all unmatched requests to. Defaults to '/api/app' if\n * not given. Disables index path behavior if false is given.\n */\n indexPath?: string | false;\n}\n\n/**\n * The default implementation of the {@link @backstage/backend-plugin-api#RootHttpRouterService} interface for\n * {@link @backstage/backend-plugin-api#coreServices.rootHttpRouter}.\n *\n * @public\n */\nexport class DefaultRootHttpRouter implements RootHttpRouterService {\n #indexPath?: string;\n\n #router = Router();\n #namedRoutes = Router();\n #indexRouter = Router();\n #existingPaths = new Array<string>();\n\n static create(options?: DefaultRootHttpRouterOptions) {\n let indexPath;\n if (options?.indexPath === false) {\n indexPath = undefined;\n } else if (options?.indexPath === undefined) {\n indexPath = '/api/app';\n } else if (options?.indexPath === '') {\n throw new Error('indexPath option may not be an empty string');\n } else {\n indexPath = options.indexPath;\n }\n return new DefaultRootHttpRouter(indexPath);\n }\n\n private constructor(indexPath?: string) {\n this.#indexPath = indexPath;\n this.#router.use(this.#namedRoutes);\n\n // Any request with a /api/ prefix will skip the index router, even if no named router matches\n this.#router.use('/api/', (_req, _res, next) => {\n next('router');\n });\n\n if (this.#indexPath) {\n this.#router.use(this.#indexRouter);\n }\n }\n\n use(path: string, handler: Handler) {\n if (path.match(/^[/\\s]*$/)) {\n throw new Error(`Root router path may not be empty`);\n }\n const conflictingPath = this.#findConflictingPath(path);\n if (conflictingPath) {\n throw new Error(\n `Path ${path} conflicts with the existing path ${conflictingPath}`,\n );\n }\n this.#existingPaths.push(path);\n this.#namedRoutes.use(path, handler);\n\n if (this.#indexPath === path) {\n this.#indexRouter.use(handler);\n }\n }\n\n handler(): Handler {\n return this.#router;\n }\n\n #findConflictingPath(newPath: string): string | undefined {\n const normalizedNewPath = normalizePath(newPath);\n for (const path of this.#existingPaths) {\n const normalizedPath = normalizePath(path);\n if (normalizedPath.startsWith(normalizedNewPath)) {\n return path;\n }\n if (normalizedNewPath.startsWith(normalizedPath)) {\n return path;\n }\n }\n return undefined;\n }\n}\n"],"names":["trimEnd","Router"],"mappings":";;;;;;;;;AAoBA,SAAS,cAAc,IAAA,EAAsB;AAC3C,EAAA,OAAO,GAAGA,wBAAA,CAAQ,IAAA,EAAM,GAAG,CAAC,IAAI,WAAA,EAAY;AAC9C;AAqBO,MAAM,qBAAA,CAAuD;AAAA,EAClE,UAAA;AAAA,EAEA,UAAUC,cAAA,EAAO;AAAA,EACjB,eAAeA,cAAA,EAAO;AAAA,EACtB,eAAeA,cAAA,EAAO;AAAA,EACtB,cAAA,GAAiB,IAAI,KAAA,EAAc;AAAA,EAEnC,OAAO,OAAO,OAAA,EAAwC;AACpD,IAAA,IAAI,SAAA;AACJ,IAAA,IAAI,OAAA,EAAS,cAAc,KAAA,EAAO;AAChC,MAAA,SAAA,GAAY,MAAA;AAAA,IACd,CAAA,MAAA,IAAW,OAAA,EAAS,SAAA,KAAc,MAAA,EAAW;AAC3C,MAAA,SAAA,GAAY,UAAA;AAAA,IACd,CAAA,MAAA,IAAW,OAAA,EAAS,SAAA,KAAc,EAAA,EAAI;AACpC,MAAA,MAAM,IAAI,MAAM,6CAA6C,CAAA;AAAA,IAC/D,CAAA,MAAO;AACL,MAAA,SAAA,GAAY,OAAA,CAAQ,SAAA;AAAA,IACtB;AACA,IAAA,OAAO,IAAI,sBAAsB,SAAS,CAAA;AAAA,EAC5C;AAAA,EAEQ,YAAY,SAAA,EAAoB;AACtC,IAAA,IAAA,CAAK,UAAA,GAAa,SAAA;AAClB,IAAA,IAAA,CAAK,OAAA,CAAQ,GAAA,CAAI,IAAA,CAAK,YAAY,CAAA;AAGlC,IAAA,IAAA,CAAK,QAAQ,GAAA,CAAI,OAAA,EAAS,CAAC,IAAA,EAAM,MAAM,IAAA,KAAS;AAC9C,MAAA,IAAA,CAAK,QAAQ,CAAA;AAAA,IACf,CAAC,CAAA;AAED,IAAA,IAAI,KAAK,UAAA,EAAY;AACnB,MAAA,IAAA,CAAK,OAAA,CAAQ,GAAA,CAAI,IAAA,CAAK,YAAY,CAAA;AAAA,IACpC;AAAA,EACF;AAAA,EAEA,GAAA,CAAI,MAAc,OAAA,EAAkB;AAClC,IAAA,IAAI,IAAA,CAAK,KAAA,CAAM,UAAU,CAAA,EAAG;AAC1B,MAAA,MAAM,IAAI,MAAM,CAAA,iCAAA,CAAmC,CAAA;AAAA,IACrD;AACA,IAAA,MAAM,eAAA,GAAkB,IAAA,CAAK,oBAAA,CAAqB,IAAI,CAAA;AACtD,IAAA,IAAI,eAAA,EAAiB;AACnB,MAAA,MAAM,IAAI,KAAA;AAAA,QACR,CAAA,KAAA,EAAQ,IAAI,CAAA,kCAAA,EAAqC,eAAe,CAAA;AAAA,OAClE;AAAA,IACF;AACA,IAAA,IAAA,CAAK,cAAA,CAAe,KAAK,IAAI,CAAA;AAC7B,IAAA,IAAA,CAAK,YAAA,CAAa,GAAA,CAAI,IAAA,EAAM,OAAO,CAAA;AAEnC,IAAA,IAAI,IAAA,CAAK,eAAe,IAAA,EAAM;AAC5B,MAAA,IAAA,CAAK,YAAA,CAAa,IAAI,OAAO,CAAA;AAAA,IAC/B;AAAA,EACF;AAAA,EAEA,OAAA,GAAmB;AACjB,IAAA,OAAO,IAAA,CAAK,OAAA;AAAA,EACd;AAAA,EAEA,qBAAqB,OAAA,EAAqC;AACxD,IAAA,MAAM,iBAAA,GAAoB,cAAc,OAAO,CAAA;AAC/C,IAAA,KAAA,MAAW,IAAA,IAAQ,KAAK,cAAA,EAAgB;AACtC,MAAA,MAAM,cAAA,GAAiB,cAAc,IAAI,CAAA;AACzC,MAAA,IAAI,cAAA,CAAe,UAAA,CAAW,iBAAiB,CAAA,EAAG;AAChD,QAAA,OAAO,IAAA;AAAA,MACT;AACA,MAAA,IAAI,iBAAA,CAAkB,UAAA,CAAW,cAAc,CAAA,EAAG;AAChD,QAAA,OAAO,IAAA;AAAA,MACT;AAAA,IACF;AACA,IAAA,OAAO,MAAA;AAAA,EACT;AACF;;;;"}
@@ -97,19 +97,34 @@ class AwsCodeCommitUrlReader {
97
97
  }
98
98
  const accessKeyId = integration.config.accessKeyId;
99
99
  const secretAccessKey = integration.config.secretAccessKey;
100
- let explicitCredentials;
100
+ const roleArn = integration.config.roleArn;
101
101
  if (accessKeyId && secretAccessKey) {
102
- explicitCredentials = AwsCodeCommitUrlReader.buildStaticCredentials(
102
+ const explicitCredentials = AwsCodeCommitUrlReader.buildStaticCredentials(
103
103
  accessKeyId,
104
104
  secretAccessKey
105
105
  );
106
- } else {
107
- explicitCredentials = (await credsManager.getCredentialProvider()).sdkCredentialProvider;
106
+ if (roleArn) {
107
+ return credentialProviders.fromTemporaryCredentials({
108
+ masterCredentials: explicitCredentials,
109
+ params: {
110
+ RoleSessionName: "backstage-aws-code-commit-url-reader",
111
+ RoleArn: roleArn,
112
+ ExternalId: integration.config.externalId
113
+ },
114
+ clientConfig: { region }
115
+ });
116
+ }
117
+ return explicitCredentials;
108
118
  }
109
- const roleArn = integration.config.roleArn;
110
119
  if (roleArn) {
120
+ let masterCredentials;
121
+ try {
122
+ masterCredentials = (await credsManager.getCredentialProvider({ arn: roleArn })).sdkCredentialProvider;
123
+ } catch {
124
+ masterCredentials = (await credsManager.getCredentialProvider()).sdkCredentialProvider;
125
+ }
111
126
  return credentialProviders.fromTemporaryCredentials({
112
- masterCredentials: explicitCredentials,
127
+ masterCredentials,
113
128
  params: {
114
129
  RoleSessionName: "backstage-aws-code-commit-url-reader",
115
130
  RoleArn: roleArn,
@@ -118,7 +133,7 @@ class AwsCodeCommitUrlReader {
118
133
  clientConfig: { region }
119
134
  });
120
135
  }
121
- return explicitCredentials;
136
+ return (await credsManager.getCredentialProvider()).sdkCredentialProvider;
122
137
  }
123
138
  async buildCodeCommitClient(credsManager, region, integration) {
124
139
  const credentials = await AwsCodeCommitUrlReader.buildCredentials(
@@ -1 +1 @@
1
- {"version":3,"file":"AwsCodeCommitUrlReader.cjs.js","sources":["../../../../src/entrypoints/urlReader/lib/AwsCodeCommitUrlReader.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 { ReaderFactory, ReadTreeResponseFactory } from './types';\nimport {\n UrlReaderService,\n UrlReaderServiceReadTreeOptions,\n UrlReaderServiceReadTreeResponse,\n UrlReaderServiceReadUrlOptions,\n UrlReaderServiceReadUrlResponse,\n UrlReaderServiceSearchOptions,\n UrlReaderServiceSearchResponse,\n} from '@backstage/backend-plugin-api';\nimport {\n AwsCredentialsManager,\n DefaultAwsCredentialsManager,\n} from '@backstage/integration-aws-node';\nimport {\n AwsCodeCommitIntegration,\n ScmIntegrations,\n} from '@backstage/integration';\nimport { toError, ForwardedError, NotModifiedError } from '@backstage/errors';\nimport { fromTemporaryCredentials } from '@aws-sdk/credential-providers';\nimport {\n CodeCommitClient,\n GetFileCommand,\n GetFileCommandInput,\n GetFileCommandOutput,\n GetFolderCommand,\n} from '@aws-sdk/client-codecommit';\nimport { AwsCredentialIdentityProvider } from '@aws-sdk/types';\nimport { Readable } from 'node:stream';\nimport { ReadUrlResponseFactory } from './ReadUrlResponseFactory';\nimport { relative } from 'node:path/posix';\nimport { AbortController } from '@aws-sdk/abort-controller';\nimport { hasDotPathSegments } from './util';\n\nexport function parseUrl(\n url: string,\n requireGitPath: boolean = false,\n): {\n path: string;\n repositoryName: string;\n region: string;\n commitSpecifier?: string;\n} {\n const parsedUrl = new URL(url);\n\n if (parsedUrl.pathname.includes('/files/edit/')) {\n throw new Error(\n 'Please provide the view url to yaml file from CodeCommit, not the edit url',\n );\n }\n if (requireGitPath && !parsedUrl.pathname.includes('/browse/')) {\n throw new Error('Please provide full path to yaml file from CodeCommit');\n }\n\n const hostMatch = parsedUrl.host.match(\n /^([^\\.]+)\\.console\\.aws\\.amazon\\.com$/,\n );\n if (!hostMatch) {\n throw new Error(\n `Invalid AWS CodeCommit URL (unexpected host format): ${url}`,\n );\n }\n const [, region] = hostMatch;\n\n const pathMatch = parsedUrl.pathname.match(\n /^\\/codesuite\\/codecommit\\/repositories\\/([^\\/]+)\\/browse\\/((.*)\\/)?--\\/(.*)$/,\n );\n\n if (!pathMatch) {\n if (!requireGitPath) {\n const pathname = parsedUrl.pathname\n .split('/--/')[0]\n .replace('/codesuite/codecommit/repositories/', '');\n const [repositoryName, commitSpecifier] = pathname.split('/browse');\n\n return {\n region,\n repositoryName: repositoryName.replace(/^\\/|\\/$/g, ''),\n path: '/',\n commitSpecifier:\n commitSpecifier === ''\n ? undefined\n : commitSpecifier?.replace(/^\\/|\\/$/g, ''),\n };\n }\n throw new Error(\n `Invalid AWS CodeCommit URL (unexpected path format): ${url}`,\n );\n }\n const [, repositoryName, , commitSpecifier, path] = pathMatch;\n\n return {\n region,\n repositoryName,\n path,\n // the commitSpecifier is passed to AWS SDK which does not allow empty strings so replace empty string with undefined\n commitSpecifier: commitSpecifier === '' ? undefined : commitSpecifier,\n };\n}\n\n/**\n * Implements a {@link @backstage/backend-plugin-api#UrlReaderService} for AWS CodeCommit.\n *\n * @public\n */\nexport class AwsCodeCommitUrlReader implements UrlReaderService {\n static factory: ReaderFactory = ({ config, treeResponseFactory }) => {\n const integrations = ScmIntegrations.fromConfig(config);\n const credsManager = DefaultAwsCredentialsManager.fromConfig(config);\n\n return integrations.awsCodeCommit.list().map(integration => {\n const reader = new AwsCodeCommitUrlReader(credsManager, integration, {\n treeResponseFactory,\n });\n const predicate = (url: URL) => {\n return (\n url.host.endsWith(integration.config.host) &&\n url.pathname.startsWith('/codesuite/codecommit')\n );\n };\n\n return { reader, predicate };\n });\n };\n\n private readonly credsManager: AwsCredentialsManager;\n private readonly integration: AwsCodeCommitIntegration;\n private readonly deps: {\n treeResponseFactory: ReadTreeResponseFactory;\n };\n\n constructor(\n credsManager: AwsCredentialsManager,\n integration: AwsCodeCommitIntegration,\n deps: {\n treeResponseFactory: ReadTreeResponseFactory;\n },\n ) {\n this.credsManager = credsManager;\n this.integration = integration;\n this.deps = deps;\n }\n\n /**\n * If accessKeyId and secretAccessKey are missing, the standard credentials provider chain will be used:\n * https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/auth/DefaultAWSCredentialsProviderChain.html\n */\n private static buildStaticCredentials(\n accessKeyId: string,\n secretAccessKey: string,\n ): AwsCredentialIdentityProvider {\n return async () => {\n return {\n accessKeyId,\n secretAccessKey,\n };\n };\n }\n\n private static async buildCredentials(\n credsManager: AwsCredentialsManager,\n region: string,\n integration?: AwsCodeCommitIntegration,\n ): Promise<AwsCredentialIdentityProvider> {\n // Fall back to the default credential chain if neither account ID\n // nor explicit credentials are provided\n if (!integration) {\n return (await credsManager.getCredentialProvider()).sdkCredentialProvider;\n }\n\n const accessKeyId = integration.config.accessKeyId;\n const secretAccessKey = integration.config.secretAccessKey;\n let explicitCredentials: AwsCredentialIdentityProvider;\n if (accessKeyId && secretAccessKey) {\n explicitCredentials = AwsCodeCommitUrlReader.buildStaticCredentials(\n accessKeyId,\n secretAccessKey,\n );\n } else {\n explicitCredentials = (await credsManager.getCredentialProvider())\n .sdkCredentialProvider;\n }\n\n const roleArn = integration.config.roleArn;\n if (roleArn) {\n return fromTemporaryCredentials({\n masterCredentials: explicitCredentials,\n params: {\n RoleSessionName: 'backstage-aws-code-commit-url-reader',\n RoleArn: roleArn,\n ExternalId: integration.config.externalId,\n },\n clientConfig: { region },\n });\n }\n\n return explicitCredentials;\n }\n\n private async buildCodeCommitClient(\n credsManager: AwsCredentialsManager,\n region: string,\n integration: AwsCodeCommitIntegration,\n ): Promise<CodeCommitClient> {\n const credentials = await AwsCodeCommitUrlReader.buildCredentials(\n credsManager,\n region,\n integration,\n );\n\n const codeCommit = new CodeCommitClient({\n customUserAgent: 'backstage-aws-codecommit-url-reader',\n region: region,\n credentials: credentials,\n });\n return codeCommit;\n }\n\n async readUrl(\n url: string,\n options?: UrlReaderServiceReadUrlOptions,\n ): Promise<UrlReaderServiceReadUrlResponse> {\n // etag and lastModifiedAfter are not supported by the CodeCommit API\n try {\n const { path, repositoryName, region, commitSpecifier } = parseUrl(\n url,\n true,\n );\n const codeCommitClient = await this.buildCodeCommitClient(\n this.credsManager,\n region,\n this.integration,\n );\n const abortController = new AbortController();\n\n const input: GetFileCommandInput = {\n repositoryName: repositoryName,\n commitSpecifier: commitSpecifier,\n filePath: path,\n };\n\n options?.signal?.addEventListener('abort', () => abortController.abort());\n const getObjectCommand = new GetFileCommand(input);\n const response: GetFileCommandOutput = await codeCommitClient.send(\n getObjectCommand,\n {\n abortSignal: abortController.signal,\n },\n );\n\n if (options?.etag && options.etag === response.commitId) {\n throw new NotModifiedError();\n }\n\n return ReadUrlResponseFactory.fromReadable(\n Readable.from([response?.fileContent]),\n {\n etag: response.commitId,\n },\n );\n } catch (e) {\n if (e.$metadata && e.$metadata.httpStatusCode === 304) {\n throw new NotModifiedError();\n }\n if (e.name && e.name === 'NotModifiedError') {\n throw new NotModifiedError();\n }\n\n throw new ForwardedError('Could not retrieve file from CodeCommit', e);\n }\n }\n\n async readTreePath(\n codeCommitClient: CodeCommitClient,\n abortSignal: any,\n path: string,\n repositoryName: string,\n commitSpecifier?: string,\n etag?: string,\n ): Promise<string[]> {\n const getFolderCommand = new GetFolderCommand({\n folderPath: path,\n repositoryName: repositoryName,\n commitSpecifier: commitSpecifier,\n });\n const response = await codeCommitClient.send(getFolderCommand, {\n abortSignal: abortSignal,\n });\n\n if (etag && etag === response.commitId) {\n throw new NotModifiedError();\n }\n\n const output: string[] = [];\n if (response.files) {\n response.files.forEach(file => {\n if (file.absolutePath) {\n output.push(file.absolutePath);\n }\n });\n }\n if (!response.subFolders) {\n return output;\n }\n\n for (const subFolder of response.subFolders) {\n if (subFolder.absolutePath) {\n output.push(\n ...(await this.readTreePath(\n codeCommitClient,\n abortSignal,\n subFolder.absolutePath,\n repositoryName,\n commitSpecifier,\n etag,\n )),\n );\n }\n }\n return output;\n }\n\n async readTree(\n url: string,\n options?: UrlReaderServiceReadTreeOptions,\n ): Promise<UrlReaderServiceReadTreeResponse> {\n // url: https://eu-west-1.console.aws.amazon.com/codesuite/codecommit/repositories/test-stijn-delete-techdocs/browse?region=eu-west-1\n try {\n const { path, repositoryName, region, commitSpecifier } = parseUrl(url);\n const codeCommitClient = await this.buildCodeCommitClient(\n this.credsManager,\n region,\n this.integration,\n );\n\n const abortController = new AbortController();\n options?.signal?.addEventListener('abort', () => abortController.abort());\n\n const allFiles: string[] = await this.readTreePath(\n codeCommitClient,\n abortController.signal,\n path,\n repositoryName,\n commitSpecifier,\n options?.etag,\n );\n const responses = [];\n\n for (let i = 0; i < allFiles.length; i++) {\n if (hasDotPathSegments(String(allFiles[i]))) {\n continue;\n }\n const getFileCommand = new GetFileCommand({\n repositoryName: repositoryName,\n filePath: String(allFiles[i]),\n commitSpecifier: commitSpecifier,\n });\n const response = await codeCommitClient.send(getFileCommand);\n const objectData = await Readable.from([response?.fileContent]);\n\n responses.push({\n data: objectData,\n path: relative(\n path.startsWith('/') ? path : `/${path}`,\n allFiles[i].startsWith('/') ? allFiles[i] : `/${allFiles[i]}`,\n ),\n });\n }\n\n return await this.deps.treeResponseFactory.fromReadableArray(responses);\n } catch (e) {\n if (e.name && e.name === 'NotModifiedError') {\n throw new NotModifiedError();\n }\n throw new ForwardedError(\n 'Could not retrieve file tree from CodeCommit',\n e,\n );\n }\n }\n\n async search(\n url: string,\n options?: UrlReaderServiceSearchOptions,\n ): Promise<UrlReaderServiceSearchResponse> {\n const { path } = parseUrl(url, true);\n\n if (path.match(/[*?]/)) {\n throw new Error('Unsupported search pattern URL');\n }\n\n try {\n const data = await this.readUrl(url, options);\n\n return {\n files: [\n {\n url: url,\n content: data.buffer,\n lastModifiedAt: data.lastModifiedAt,\n },\n ],\n etag: data.etag ?? '',\n };\n } catch (e) {\n const error = toError(e);\n if (error.name === 'NotFoundError') {\n return {\n files: [],\n etag: '',\n };\n }\n throw error;\n }\n }\n\n toString() {\n const secretAccessKey = this.integration.config.secretAccessKey;\n return `awsCodeCommit{host=${this.integration.config.host},authed=${Boolean(\n secretAccessKey,\n )}}`;\n }\n}\n"],"names":["repositoryName","commitSpecifier","ScmIntegrations","DefaultAwsCredentialsManager","fromTemporaryCredentials","CodeCommitClient","abortController","AbortController","GetFileCommand","NotModifiedError","ReadUrlResponseFactory","Readable","ForwardedError","GetFolderCommand","hasDotPathSegments","relative","toError"],"mappings":"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"}
1
+ {"version":3,"file":"AwsCodeCommitUrlReader.cjs.js","sources":["../../../../src/entrypoints/urlReader/lib/AwsCodeCommitUrlReader.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 { ReaderFactory, ReadTreeResponseFactory } from './types';\nimport {\n UrlReaderService,\n UrlReaderServiceReadTreeOptions,\n UrlReaderServiceReadTreeResponse,\n UrlReaderServiceReadUrlOptions,\n UrlReaderServiceReadUrlResponse,\n UrlReaderServiceSearchOptions,\n UrlReaderServiceSearchResponse,\n} from '@backstage/backend-plugin-api';\nimport {\n AwsCredentialsManager,\n DefaultAwsCredentialsManager,\n} from '@backstage/integration-aws-node';\nimport {\n AwsCodeCommitIntegration,\n ScmIntegrations,\n} from '@backstage/integration';\nimport { toError, ForwardedError, NotModifiedError } from '@backstage/errors';\nimport { fromTemporaryCredentials } from '@aws-sdk/credential-providers';\nimport {\n CodeCommitClient,\n GetFileCommand,\n GetFileCommandInput,\n GetFileCommandOutput,\n GetFolderCommand,\n} from '@aws-sdk/client-codecommit';\nimport { AwsCredentialIdentityProvider } from '@aws-sdk/types';\nimport { Readable } from 'node:stream';\nimport { ReadUrlResponseFactory } from './ReadUrlResponseFactory';\nimport { relative } from 'node:path/posix';\nimport { AbortController } from '@aws-sdk/abort-controller';\nimport { hasDotPathSegments } from './util';\n\nexport function parseUrl(\n url: string,\n requireGitPath: boolean = false,\n): {\n path: string;\n repositoryName: string;\n region: string;\n commitSpecifier?: string;\n} {\n const parsedUrl = new URL(url);\n\n if (parsedUrl.pathname.includes('/files/edit/')) {\n throw new Error(\n 'Please provide the view url to yaml file from CodeCommit, not the edit url',\n );\n }\n if (requireGitPath && !parsedUrl.pathname.includes('/browse/')) {\n throw new Error('Please provide full path to yaml file from CodeCommit');\n }\n\n const hostMatch = parsedUrl.host.match(\n /^([^\\.]+)\\.console\\.aws\\.amazon\\.com$/,\n );\n if (!hostMatch) {\n throw new Error(\n `Invalid AWS CodeCommit URL (unexpected host format): ${url}`,\n );\n }\n const [, region] = hostMatch;\n\n const pathMatch = parsedUrl.pathname.match(\n /^\\/codesuite\\/codecommit\\/repositories\\/([^\\/]+)\\/browse\\/((.*)\\/)?--\\/(.*)$/,\n );\n\n if (!pathMatch) {\n if (!requireGitPath) {\n const pathname = parsedUrl.pathname\n .split('/--/')[0]\n .replace('/codesuite/codecommit/repositories/', '');\n const [repositoryName, commitSpecifier] = pathname.split('/browse');\n\n return {\n region,\n repositoryName: repositoryName.replace(/^\\/|\\/$/g, ''),\n path: '/',\n commitSpecifier:\n commitSpecifier === ''\n ? undefined\n : commitSpecifier?.replace(/^\\/|\\/$/g, ''),\n };\n }\n throw new Error(\n `Invalid AWS CodeCommit URL (unexpected path format): ${url}`,\n );\n }\n const [, repositoryName, , commitSpecifier, path] = pathMatch;\n\n return {\n region,\n repositoryName,\n path,\n // the commitSpecifier is passed to AWS SDK which does not allow empty strings so replace empty string with undefined\n commitSpecifier: commitSpecifier === '' ? undefined : commitSpecifier,\n };\n}\n\n/**\n * Implements a {@link @backstage/backend-plugin-api#UrlReaderService} for AWS CodeCommit.\n *\n * @public\n */\nexport class AwsCodeCommitUrlReader implements UrlReaderService {\n static factory: ReaderFactory = ({ config, treeResponseFactory }) => {\n const integrations = ScmIntegrations.fromConfig(config);\n const credsManager = DefaultAwsCredentialsManager.fromConfig(config);\n\n return integrations.awsCodeCommit.list().map(integration => {\n const reader = new AwsCodeCommitUrlReader(credsManager, integration, {\n treeResponseFactory,\n });\n const predicate = (url: URL) => {\n return (\n url.host.endsWith(integration.config.host) &&\n url.pathname.startsWith('/codesuite/codecommit')\n );\n };\n\n return { reader, predicate };\n });\n };\n\n private readonly credsManager: AwsCredentialsManager;\n private readonly integration: AwsCodeCommitIntegration;\n private readonly deps: {\n treeResponseFactory: ReadTreeResponseFactory;\n };\n\n constructor(\n credsManager: AwsCredentialsManager,\n integration: AwsCodeCommitIntegration,\n deps: {\n treeResponseFactory: ReadTreeResponseFactory;\n },\n ) {\n this.credsManager = credsManager;\n this.integration = integration;\n this.deps = deps;\n }\n\n /**\n * If accessKeyId and secretAccessKey are missing, the standard credentials provider chain will be used:\n * https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/auth/DefaultAWSCredentialsProviderChain.html\n */\n private static buildStaticCredentials(\n accessKeyId: string,\n secretAccessKey: string,\n ): AwsCredentialIdentityProvider {\n return async () => {\n return {\n accessKeyId,\n secretAccessKey,\n };\n };\n }\n\n private static async buildCredentials(\n credsManager: AwsCredentialsManager,\n region: string,\n integration?: AwsCodeCommitIntegration,\n ): Promise<AwsCredentialIdentityProvider> {\n // Fall back to the default credential chain if neither account ID\n // nor explicit credentials are provided\n if (!integration) {\n return (await credsManager.getCredentialProvider()).sdkCredentialProvider;\n }\n\n const accessKeyId = integration.config.accessKeyId;\n const secretAccessKey = integration.config.secretAccessKey;\n const roleArn = integration.config.roleArn;\n\n if (accessKeyId && secretAccessKey) {\n const explicitCredentials = AwsCodeCommitUrlReader.buildStaticCredentials(\n accessKeyId,\n secretAccessKey,\n );\n if (roleArn) {\n return fromTemporaryCredentials({\n masterCredentials: explicitCredentials,\n params: {\n RoleSessionName: 'backstage-aws-code-commit-url-reader',\n RoleArn: roleArn,\n ExternalId: integration.config.externalId,\n },\n clientConfig: { region },\n });\n }\n return explicitCredentials;\n }\n\n if (roleArn) {\n let masterCredentials: AwsCredentialIdentityProvider;\n try {\n masterCredentials = (\n await credsManager.getCredentialProvider({ arn: roleArn })\n ).sdkCredentialProvider;\n } catch {\n // No account-specific config for this ARN; fall back to default credentials\n masterCredentials = (await credsManager.getCredentialProvider())\n .sdkCredentialProvider;\n }\n return fromTemporaryCredentials({\n masterCredentials,\n params: {\n RoleSessionName: 'backstage-aws-code-commit-url-reader',\n RoleArn: roleArn,\n ExternalId: integration.config.externalId,\n },\n clientConfig: { region },\n });\n }\n\n return (await credsManager.getCredentialProvider()).sdkCredentialProvider;\n }\n\n private async buildCodeCommitClient(\n credsManager: AwsCredentialsManager,\n region: string,\n integration: AwsCodeCommitIntegration,\n ): Promise<CodeCommitClient> {\n const credentials = await AwsCodeCommitUrlReader.buildCredentials(\n credsManager,\n region,\n integration,\n );\n\n const codeCommit = new CodeCommitClient({\n customUserAgent: 'backstage-aws-codecommit-url-reader',\n region: region,\n credentials: credentials,\n });\n return codeCommit;\n }\n\n async readUrl(\n url: string,\n options?: UrlReaderServiceReadUrlOptions,\n ): Promise<UrlReaderServiceReadUrlResponse> {\n // etag and lastModifiedAfter are not supported by the CodeCommit API\n try {\n const { path, repositoryName, region, commitSpecifier } = parseUrl(\n url,\n true,\n );\n const codeCommitClient = await this.buildCodeCommitClient(\n this.credsManager,\n region,\n this.integration,\n );\n const abortController = new AbortController();\n\n const input: GetFileCommandInput = {\n repositoryName: repositoryName,\n commitSpecifier: commitSpecifier,\n filePath: path,\n };\n\n options?.signal?.addEventListener('abort', () => abortController.abort());\n const getObjectCommand = new GetFileCommand(input);\n const response: GetFileCommandOutput = await codeCommitClient.send(\n getObjectCommand,\n {\n abortSignal: abortController.signal,\n },\n );\n\n if (options?.etag && options.etag === response.commitId) {\n throw new NotModifiedError();\n }\n\n return ReadUrlResponseFactory.fromReadable(\n Readable.from([response?.fileContent]),\n {\n etag: response.commitId,\n },\n );\n } catch (e) {\n if (e.$metadata && e.$metadata.httpStatusCode === 304) {\n throw new NotModifiedError();\n }\n if (e.name && e.name === 'NotModifiedError') {\n throw new NotModifiedError();\n }\n\n throw new ForwardedError('Could not retrieve file from CodeCommit', e);\n }\n }\n\n async readTreePath(\n codeCommitClient: CodeCommitClient,\n abortSignal: any,\n path: string,\n repositoryName: string,\n commitSpecifier?: string,\n etag?: string,\n ): Promise<string[]> {\n const getFolderCommand = new GetFolderCommand({\n folderPath: path,\n repositoryName: repositoryName,\n commitSpecifier: commitSpecifier,\n });\n const response = await codeCommitClient.send(getFolderCommand, {\n abortSignal: abortSignal,\n });\n\n if (etag && etag === response.commitId) {\n throw new NotModifiedError();\n }\n\n const output: string[] = [];\n if (response.files) {\n response.files.forEach(file => {\n if (file.absolutePath) {\n output.push(file.absolutePath);\n }\n });\n }\n if (!response.subFolders) {\n return output;\n }\n\n for (const subFolder of response.subFolders) {\n if (subFolder.absolutePath) {\n output.push(\n ...(await this.readTreePath(\n codeCommitClient,\n abortSignal,\n subFolder.absolutePath,\n repositoryName,\n commitSpecifier,\n etag,\n )),\n );\n }\n }\n return output;\n }\n\n async readTree(\n url: string,\n options?: UrlReaderServiceReadTreeOptions,\n ): Promise<UrlReaderServiceReadTreeResponse> {\n // url: https://eu-west-1.console.aws.amazon.com/codesuite/codecommit/repositories/test-stijn-delete-techdocs/browse?region=eu-west-1\n try {\n const { path, repositoryName, region, commitSpecifier } = parseUrl(url);\n const codeCommitClient = await this.buildCodeCommitClient(\n this.credsManager,\n region,\n this.integration,\n );\n\n const abortController = new AbortController();\n options?.signal?.addEventListener('abort', () => abortController.abort());\n\n const allFiles: string[] = await this.readTreePath(\n codeCommitClient,\n abortController.signal,\n path,\n repositoryName,\n commitSpecifier,\n options?.etag,\n );\n const responses = [];\n\n for (let i = 0; i < allFiles.length; i++) {\n if (hasDotPathSegments(String(allFiles[i]))) {\n continue;\n }\n const getFileCommand = new GetFileCommand({\n repositoryName: repositoryName,\n filePath: String(allFiles[i]),\n commitSpecifier: commitSpecifier,\n });\n const response = await codeCommitClient.send(getFileCommand);\n const objectData = await Readable.from([response?.fileContent]);\n\n responses.push({\n data: objectData,\n path: relative(\n path.startsWith('/') ? path : `/${path}`,\n allFiles[i].startsWith('/') ? allFiles[i] : `/${allFiles[i]}`,\n ),\n });\n }\n\n return await this.deps.treeResponseFactory.fromReadableArray(responses);\n } catch (e) {\n if (e.name && e.name === 'NotModifiedError') {\n throw new NotModifiedError();\n }\n throw new ForwardedError(\n 'Could not retrieve file tree from CodeCommit',\n e,\n );\n }\n }\n\n async search(\n url: string,\n options?: UrlReaderServiceSearchOptions,\n ): Promise<UrlReaderServiceSearchResponse> {\n const { path } = parseUrl(url, true);\n\n if (path.match(/[*?]/)) {\n throw new Error('Unsupported search pattern URL');\n }\n\n try {\n const data = await this.readUrl(url, options);\n\n return {\n files: [\n {\n url: url,\n content: data.buffer,\n lastModifiedAt: data.lastModifiedAt,\n },\n ],\n etag: data.etag ?? '',\n };\n } catch (e) {\n const error = toError(e);\n if (error.name === 'NotFoundError') {\n return {\n files: [],\n etag: '',\n };\n }\n throw error;\n }\n }\n\n toString() {\n const secretAccessKey = this.integration.config.secretAccessKey;\n return `awsCodeCommit{host=${this.integration.config.host},authed=${Boolean(\n secretAccessKey,\n )}}`;\n }\n}\n"],"names":["repositoryName","commitSpecifier","ScmIntegrations","DefaultAwsCredentialsManager","fromTemporaryCredentials","CodeCommitClient","abortController","AbortController","GetFileCommand","NotModifiedError","ReadUrlResponseFactory","Readable","ForwardedError","GetFolderCommand","hasDotPathSegments","relative","toError"],"mappings":"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"}
@@ -123,19 +123,34 @@ class AwsS3UrlReader {
123
123
  }
124
124
  const accessKeyId = integration.config.accessKeyId;
125
125
  const secretAccessKey = integration.config.secretAccessKey;
126
- let explicitCredentials;
126
+ const roleArn = integration.config.roleArn;
127
127
  if (accessKeyId && secretAccessKey) {
128
- explicitCredentials = AwsS3UrlReader.buildStaticCredentials(
128
+ const explicitCredentials = AwsS3UrlReader.buildStaticCredentials(
129
129
  accessKeyId,
130
130
  secretAccessKey
131
131
  );
132
- } else {
133
- explicitCredentials = (await credsManager.getCredentialProvider()).sdkCredentialProvider;
132
+ if (roleArn) {
133
+ return credentialProviders.fromTemporaryCredentials({
134
+ masterCredentials: explicitCredentials,
135
+ params: {
136
+ RoleSessionName: "backstage-aws-s3-url-reader",
137
+ RoleArn: roleArn,
138
+ ExternalId: integration.config.externalId
139
+ },
140
+ clientConfig: { region }
141
+ });
142
+ }
143
+ return explicitCredentials;
134
144
  }
135
- const roleArn = integration.config.roleArn;
136
145
  if (roleArn) {
146
+ let masterCredentials;
147
+ try {
148
+ masterCredentials = (await credsManager.getCredentialProvider({ arn: roleArn })).sdkCredentialProvider;
149
+ } catch {
150
+ masterCredentials = (await credsManager.getCredentialProvider()).sdkCredentialProvider;
151
+ }
137
152
  return credentialProviders.fromTemporaryCredentials({
138
- masterCredentials: explicitCredentials,
153
+ masterCredentials,
139
154
  params: {
140
155
  RoleSessionName: "backstage-aws-s3-url-reader",
141
156
  RoleArn: roleArn,
@@ -144,7 +159,7 @@ class AwsS3UrlReader {
144
159
  clientConfig: { region }
145
160
  });
146
161
  }
147
- return explicitCredentials;
162
+ return (await credsManager.getCredentialProvider()).sdkCredentialProvider;
148
163
  }
149
164
  async buildS3Client(credsManager, region, integration) {
150
165
  const credentials = await AwsS3UrlReader.buildCredentials(
@@ -1 +1 @@
1
- {"version":3,"file":"AwsS3UrlReader.cjs.js","sources":["../../../../src/entrypoints/urlReader/lib/AwsS3UrlReader.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 UrlReaderService,\n UrlReaderServiceReadTreeOptions,\n UrlReaderServiceReadTreeResponse,\n UrlReaderServiceReadUrlOptions,\n UrlReaderServiceReadUrlResponse,\n UrlReaderServiceSearchOptions,\n UrlReaderServiceSearchResponse,\n} from '@backstage/backend-plugin-api';\nimport { ReaderFactory, ReadTreeResponseFactory } from './types';\nimport {\n AwsCredentialsManager,\n DefaultAwsCredentialsManager,\n} from '@backstage/integration-aws-node';\nimport {\n AwsS3Integration,\n ScmIntegrations,\n AwsS3IntegrationConfig,\n} from '@backstage/integration';\nimport { toError, ForwardedError, NotModifiedError } from '@backstage/errors';\nimport { fromTemporaryCredentials } from '@aws-sdk/credential-providers';\nimport { AwsCredentialIdentityProvider } from '@aws-sdk/types';\nimport {\n S3Client,\n ListObjectsV2Command,\n ListObjectsV2CommandOutput,\n GetObjectCommand,\n GetObjectCommandInput,\n} from '@aws-sdk/client-s3';\nimport { AbortController } from '@aws-sdk/abort-controller';\nimport { ReadUrlResponseFactory } from './ReadUrlResponseFactory';\nimport { Readable } from 'node:stream';\nimport { relative } from 'node:path/posix';\nimport { hasDotPathSegments, isUrlPathWithoutDotSegments } from './util';\n\nexport const DEFAULT_REGION = 'us-east-1';\n\n/**\n * Standard AWS S3 URLs:\n * Path style: https://s3.(region).amazonaws.com/(bucket)/(key)\n * Path style (old): https://s3-(region).amazonaws.com/(bucket)/(key)\n * Virtual hosted style: https://(bucket).s3.(region).amazonaws.com/(key)\n *\n * AWS PrivateLink (VPC endpoint) S3 URLs:\n * https://(bucket).bucket.(vpce-id).s3.(region).vpce.amazonaws.com/(key)\n *\n * Both forms may have an optional .cn suffix for China regions.\n *\n * See https://docs.aws.amazon.com/AmazonS3/latest/userguide/VirtualHosting.html\n * See https://docs.aws.amazon.com/AmazonS3/latest/userguide/privatelink-interface-endpoints.html\n */\n\nconst STANDARD_AWS_HOST_RE =\n /^(?:(?<bucket>[a-z0-9.-]+)\\.)?s3(?:[.-](?<region>[a-z0-9-]+))?\\.amazonaws\\.com(?:\\.cn)?$/;\n\nconst VPC_ENDPOINT_HOST_RE =\n /^(?<bucket>[a-z0-9.-]+)\\.bucket\\.(?<vpceId>vpce-[a-z0-9-]+)\\.s3[.-](?<region>[a-z0-9-]+)\\.vpce\\.amazonaws\\.com(?:\\.cn)?$/;\n\nexport function parseUrl(\n url: string,\n config: AwsS3IntegrationConfig,\n): { path: string; bucket: string; region: string } {\n if (!isUrlPathWithoutDotSegments(url)) {\n throw new Error(`Invalid AWS S3 URL ${url}`);\n }\n\n const parsedUrl = new URL(url);\n const pathname = parsedUrl.pathname\n .substring(1)\n .split('/')\n .map(decodeURIComponent)\n .join('/');\n const host = parsedUrl.host;\n\n if (isAmazonHost(config.host)) {\n return parseAmazonUrl(url, host, pathname, config);\n }\n\n const usePathStyle =\n config.s3ForcePathStyle || host.length === config.host.length;\n\n if (usePathStyle) {\n const slashIndex = pathname.indexOf('/');\n if (slashIndex < 0) {\n throw new Error(\n `Invalid path-style AWS S3 URL ${url}, does not contain bucket in the path`,\n );\n }\n\n return {\n path: pathname.substring(slashIndex + 1),\n bucket: pathname.substring(0, slashIndex),\n region: DEFAULT_REGION,\n };\n }\n\n return {\n path: pathname,\n bucket: host.substring(0, host.length - config.host.length - 1),\n region: DEFAULT_REGION,\n };\n}\n\nfunction isAmazonHost(host: string): boolean {\n return (\n host === 'amazonaws.com' ||\n host === 'amazonaws.com.cn' ||\n host.endsWith('.amazonaws.com') ||\n host.endsWith('.amazonaws.com.cn')\n );\n}\n\nfunction parseAmazonUrl(\n url: string,\n host: string,\n pathname: string,\n config: AwsS3IntegrationConfig,\n): { path: string; bucket: string; region: string } {\n // Try VPC PrivateLink format first (more specific pattern)\n const vpcMatch = host.match(VPC_ENDPOINT_HOST_RE);\n if (vpcMatch?.groups) {\n if (config.s3ForcePathStyle) {\n throw new Error(\n `Invalid AWS S3 URL ${url} - path style access is not supported for VPC endpoint URLs`,\n );\n }\n return {\n path: pathname,\n bucket: vpcMatch.groups.bucket,\n region: vpcMatch.groups.region,\n };\n }\n\n // Try standard AWS S3 format\n const stdMatch = host.match(STANDARD_AWS_HOST_RE);\n if (!stdMatch?.groups) {\n throw new Error(`Invalid AWS S3 URL ${url}`);\n }\n\n const { bucket: hostBucket, region: hostRegion } = stdMatch.groups;\n\n if (config.s3ForcePathStyle || !hostBucket) {\n const slashIndex = pathname.indexOf('/');\n if (slashIndex < 0) {\n throw new Error(\n `Invalid path-style AWS S3 URL ${url}, does not contain bucket in the path`,\n );\n }\n return {\n path: pathname.substring(slashIndex + 1),\n bucket: pathname.substring(0, slashIndex),\n region: hostRegion ?? DEFAULT_REGION,\n };\n }\n\n return {\n path: pathname,\n bucket: hostBucket,\n region: hostRegion ?? DEFAULT_REGION,\n };\n}\n\n/**\n * Implements a {@link @backstage/backend-plugin-api#UrlReaderService} for AWS S3 buckets.\n *\n * @public\n */\nexport class AwsS3UrlReader implements UrlReaderService {\n static factory: ReaderFactory = ({ config, treeResponseFactory }) => {\n const integrations = ScmIntegrations.fromConfig(config);\n const credsManager = DefaultAwsCredentialsManager.fromConfig(config);\n\n return integrations.awsS3.list().map(integration => {\n const reader = new AwsS3UrlReader(credsManager, integration, {\n treeResponseFactory,\n });\n const predicate = (url: URL) =>\n url.host.endsWith(integration.config.host);\n return { reader, predicate };\n });\n };\n\n private readonly credsManager: AwsCredentialsManager;\n private readonly integration: AwsS3Integration;\n private readonly deps: {\n treeResponseFactory: ReadTreeResponseFactory;\n };\n\n constructor(\n credsManager: AwsCredentialsManager,\n integration: AwsS3Integration,\n deps: {\n treeResponseFactory: ReadTreeResponseFactory;\n },\n ) {\n this.credsManager = credsManager;\n this.integration = integration;\n this.deps = deps;\n }\n\n /**\n * If accessKeyId and secretAccessKey are missing, the standard credentials provider chain will be used:\n * https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/auth/DefaultAWSCredentialsProviderChain.html\n */\n private static buildStaticCredentials(\n accessKeyId: string,\n secretAccessKey: string,\n ): AwsCredentialIdentityProvider {\n return async () => {\n return {\n accessKeyId,\n secretAccessKey,\n };\n };\n }\n\n private static async buildCredentials(\n credsManager: AwsCredentialsManager,\n region: string,\n integration?: AwsS3Integration,\n ): Promise<AwsCredentialIdentityProvider> {\n // Fall back to the default credential chain if neither account ID\n // nor explicit credentials are provided\n if (!integration) {\n return (await credsManager.getCredentialProvider()).sdkCredentialProvider;\n }\n\n const accessKeyId = integration.config.accessKeyId;\n const secretAccessKey = integration.config.secretAccessKey;\n let explicitCredentials: AwsCredentialIdentityProvider;\n if (accessKeyId && secretAccessKey) {\n explicitCredentials = AwsS3UrlReader.buildStaticCredentials(\n accessKeyId,\n secretAccessKey,\n );\n } else {\n explicitCredentials = (await credsManager.getCredentialProvider())\n .sdkCredentialProvider;\n }\n\n const roleArn = integration.config.roleArn;\n if (roleArn) {\n return fromTemporaryCredentials({\n masterCredentials: explicitCredentials,\n params: {\n RoleSessionName: 'backstage-aws-s3-url-reader',\n RoleArn: roleArn,\n ExternalId: integration.config.externalId,\n },\n clientConfig: { region },\n });\n }\n\n return explicitCredentials;\n }\n\n private async buildS3Client(\n credsManager: AwsCredentialsManager,\n region: string,\n integration: AwsS3Integration,\n ): Promise<S3Client> {\n const credentials = await AwsS3UrlReader.buildCredentials(\n credsManager,\n region,\n integration,\n );\n\n const s3 = new S3Client({\n customUserAgent: 'backstage-aws-s3-url-reader',\n region: region,\n credentials: credentials,\n endpoint: integration.config.endpoint,\n forcePathStyle: integration.config.s3ForcePathStyle,\n });\n return s3;\n }\n\n private async retrieveS3ObjectData(stream: Readable): Promise<Readable> {\n return new Promise((resolve, reject) => {\n try {\n const chunks: any[] = [];\n stream.on('data', chunk => chunks.push(chunk));\n stream.on('error', (e: Error) =>\n reject(new ForwardedError('Unable to read stream', e)),\n );\n stream.on('end', () => resolve(Readable.from(Buffer.concat(chunks))));\n } catch (e) {\n throw new ForwardedError('Unable to parse the response data', e);\n }\n });\n }\n\n async read(url: string): Promise<Buffer> {\n const response = await this.readUrl(url);\n return response.buffer();\n }\n\n async readUrl(\n url: string,\n options?: UrlReaderServiceReadUrlOptions,\n ): Promise<UrlReaderServiceReadUrlResponse> {\n const { etag, lastModifiedAfter } = options ?? {};\n\n try {\n const { path, bucket, region } = parseUrl(url, this.integration.config);\n const s3Client = await this.buildS3Client(\n this.credsManager,\n region,\n this.integration,\n );\n const abortController = new AbortController();\n\n const params: GetObjectCommandInput = {\n Bucket: bucket,\n Key: path,\n ...(etag && { IfNoneMatch: etag }),\n ...(lastModifiedAfter && {\n IfModifiedSince: lastModifiedAfter,\n }),\n };\n\n options?.signal?.addEventListener('abort', () => abortController.abort());\n const getObjectCommand = new GetObjectCommand(params);\n const response = await s3Client.send(getObjectCommand, {\n abortSignal: abortController.signal,\n });\n\n const s3ObjectData = await this.retrieveS3ObjectData(\n response.Body as Readable,\n );\n\n return ReadUrlResponseFactory.fromReadable(s3ObjectData, {\n etag: response.ETag,\n lastModifiedAt: response.LastModified,\n });\n } catch (e) {\n if (e.$metadata && e.$metadata.httpStatusCode === 304) {\n throw new NotModifiedError();\n }\n\n throw new ForwardedError('Could not retrieve file from S3', e);\n }\n }\n\n async readTree(\n url: string,\n options?: UrlReaderServiceReadTreeOptions,\n ): Promise<UrlReaderServiceReadTreeResponse> {\n try {\n const { path, bucket, region } = parseUrl(url, this.integration.config);\n const s3Client = await this.buildS3Client(\n this.credsManager,\n region,\n this.integration,\n );\n const abortController = new AbortController();\n const allObjects: String[] = [];\n const responses = [];\n let continuationToken: string | undefined;\n let output: ListObjectsV2CommandOutput;\n do {\n const listObjectsV2Command = new ListObjectsV2Command({\n Bucket: bucket,\n ContinuationToken: continuationToken,\n Prefix: path,\n });\n options?.signal?.addEventListener('abort', () =>\n abortController.abort(),\n );\n output = await s3Client.send(listObjectsV2Command, {\n abortSignal: abortController.signal,\n });\n if (output.Contents) {\n output.Contents.forEach(contents => {\n allObjects.push(contents.Key!);\n });\n }\n continuationToken = output.NextContinuationToken;\n } while (continuationToken);\n\n for (let i = 0; i < allObjects.length; i++) {\n if (hasDotPathSegments(String(allObjects[i]))) {\n continue;\n }\n const getObjectCommand = new GetObjectCommand({\n Bucket: bucket,\n Key: String(allObjects[i]),\n });\n const response = await s3Client.send(getObjectCommand);\n const s3ObjectData = await this.retrieveS3ObjectData(\n response.Body as Readable,\n );\n\n responses.push({\n data: s3ObjectData,\n path: relative(path, String(allObjects[i])),\n lastModifiedAt: response?.LastModified ?? undefined,\n });\n }\n\n return await this.deps.treeResponseFactory.fromReadableArray(responses);\n } catch (e) {\n throw new ForwardedError('Could not retrieve file tree from S3', e);\n }\n }\n\n async search(\n url: string,\n options?: UrlReaderServiceSearchOptions,\n ): Promise<UrlReaderServiceSearchResponse> {\n const { path } = parseUrl(url, this.integration.config);\n\n if (path.match(/[*?]/)) {\n throw new Error('Unsupported search pattern URL');\n }\n\n try {\n const data = await this.readUrl(url, options);\n\n return {\n files: [\n {\n url: url,\n content: data.buffer,\n lastModifiedAt: data.lastModifiedAt,\n },\n ],\n etag: data.etag ?? '',\n };\n } catch (e) {\n const error = toError(e);\n if (error.name === 'NotFoundError') {\n return {\n files: [],\n etag: '',\n };\n }\n throw error;\n }\n }\n\n toString() {\n const secretAccessKey = this.integration.config.secretAccessKey;\n return `awsS3{host=${this.integration.config.host},authed=${Boolean(\n secretAccessKey,\n )}}`;\n }\n}\n"],"names":["isUrlPathWithoutDotSegments","ScmIntegrations","DefaultAwsCredentialsManager","fromTemporaryCredentials","S3Client","ForwardedError","Readable","abortController","AbortController","GetObjectCommand","ReadUrlResponseFactory","NotModifiedError","ListObjectsV2Command","hasDotPathSegments","relative","toError"],"mappings":"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"}
1
+ {"version":3,"file":"AwsS3UrlReader.cjs.js","sources":["../../../../src/entrypoints/urlReader/lib/AwsS3UrlReader.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 UrlReaderService,\n UrlReaderServiceReadTreeOptions,\n UrlReaderServiceReadTreeResponse,\n UrlReaderServiceReadUrlOptions,\n UrlReaderServiceReadUrlResponse,\n UrlReaderServiceSearchOptions,\n UrlReaderServiceSearchResponse,\n} from '@backstage/backend-plugin-api';\nimport { ReaderFactory, ReadTreeResponseFactory } from './types';\nimport {\n AwsCredentialsManager,\n DefaultAwsCredentialsManager,\n} from '@backstage/integration-aws-node';\nimport {\n AwsS3Integration,\n ScmIntegrations,\n AwsS3IntegrationConfig,\n} from '@backstage/integration';\nimport { toError, ForwardedError, NotModifiedError } from '@backstage/errors';\nimport { fromTemporaryCredentials } from '@aws-sdk/credential-providers';\nimport { AwsCredentialIdentityProvider } from '@aws-sdk/types';\nimport {\n S3Client,\n ListObjectsV2Command,\n ListObjectsV2CommandOutput,\n GetObjectCommand,\n GetObjectCommandInput,\n} from '@aws-sdk/client-s3';\nimport { AbortController } from '@aws-sdk/abort-controller';\nimport { ReadUrlResponseFactory } from './ReadUrlResponseFactory';\nimport { Readable } from 'node:stream';\nimport { relative } from 'node:path/posix';\nimport { hasDotPathSegments, isUrlPathWithoutDotSegments } from './util';\n\nexport const DEFAULT_REGION = 'us-east-1';\n\n/**\n * Standard AWS S3 URLs:\n * Path style: https://s3.(region).amazonaws.com/(bucket)/(key)\n * Path style (old): https://s3-(region).amazonaws.com/(bucket)/(key)\n * Virtual hosted style: https://(bucket).s3.(region).amazonaws.com/(key)\n *\n * AWS PrivateLink (VPC endpoint) S3 URLs:\n * https://(bucket).bucket.(vpce-id).s3.(region).vpce.amazonaws.com/(key)\n *\n * Both forms may have an optional .cn suffix for China regions.\n *\n * See https://docs.aws.amazon.com/AmazonS3/latest/userguide/VirtualHosting.html\n * See https://docs.aws.amazon.com/AmazonS3/latest/userguide/privatelink-interface-endpoints.html\n */\n\nconst STANDARD_AWS_HOST_RE =\n /^(?:(?<bucket>[a-z0-9.-]+)\\.)?s3(?:[.-](?<region>[a-z0-9-]+))?\\.amazonaws\\.com(?:\\.cn)?$/;\n\nconst VPC_ENDPOINT_HOST_RE =\n /^(?<bucket>[a-z0-9.-]+)\\.bucket\\.(?<vpceId>vpce-[a-z0-9-]+)\\.s3[.-](?<region>[a-z0-9-]+)\\.vpce\\.amazonaws\\.com(?:\\.cn)?$/;\n\nexport function parseUrl(\n url: string,\n config: AwsS3IntegrationConfig,\n): { path: string; bucket: string; region: string } {\n if (!isUrlPathWithoutDotSegments(url)) {\n throw new Error(`Invalid AWS S3 URL ${url}`);\n }\n\n const parsedUrl = new URL(url);\n const pathname = parsedUrl.pathname\n .substring(1)\n .split('/')\n .map(decodeURIComponent)\n .join('/');\n const host = parsedUrl.host;\n\n if (isAmazonHost(config.host)) {\n return parseAmazonUrl(url, host, pathname, config);\n }\n\n const usePathStyle =\n config.s3ForcePathStyle || host.length === config.host.length;\n\n if (usePathStyle) {\n const slashIndex = pathname.indexOf('/');\n if (slashIndex < 0) {\n throw new Error(\n `Invalid path-style AWS S3 URL ${url}, does not contain bucket in the path`,\n );\n }\n\n return {\n path: pathname.substring(slashIndex + 1),\n bucket: pathname.substring(0, slashIndex),\n region: DEFAULT_REGION,\n };\n }\n\n return {\n path: pathname,\n bucket: host.substring(0, host.length - config.host.length - 1),\n region: DEFAULT_REGION,\n };\n}\n\nfunction isAmazonHost(host: string): boolean {\n return (\n host === 'amazonaws.com' ||\n host === 'amazonaws.com.cn' ||\n host.endsWith('.amazonaws.com') ||\n host.endsWith('.amazonaws.com.cn')\n );\n}\n\nfunction parseAmazonUrl(\n url: string,\n host: string,\n pathname: string,\n config: AwsS3IntegrationConfig,\n): { path: string; bucket: string; region: string } {\n // Try VPC PrivateLink format first (more specific pattern)\n const vpcMatch = host.match(VPC_ENDPOINT_HOST_RE);\n if (vpcMatch?.groups) {\n if (config.s3ForcePathStyle) {\n throw new Error(\n `Invalid AWS S3 URL ${url} - path style access is not supported for VPC endpoint URLs`,\n );\n }\n return {\n path: pathname,\n bucket: vpcMatch.groups.bucket,\n region: vpcMatch.groups.region,\n };\n }\n\n // Try standard AWS S3 format\n const stdMatch = host.match(STANDARD_AWS_HOST_RE);\n if (!stdMatch?.groups) {\n throw new Error(`Invalid AWS S3 URL ${url}`);\n }\n\n const { bucket: hostBucket, region: hostRegion } = stdMatch.groups;\n\n if (config.s3ForcePathStyle || !hostBucket) {\n const slashIndex = pathname.indexOf('/');\n if (slashIndex < 0) {\n throw new Error(\n `Invalid path-style AWS S3 URL ${url}, does not contain bucket in the path`,\n );\n }\n return {\n path: pathname.substring(slashIndex + 1),\n bucket: pathname.substring(0, slashIndex),\n region: hostRegion ?? DEFAULT_REGION,\n };\n }\n\n return {\n path: pathname,\n bucket: hostBucket,\n region: hostRegion ?? DEFAULT_REGION,\n };\n}\n\n/**\n * Implements a {@link @backstage/backend-plugin-api#UrlReaderService} for AWS S3 buckets.\n *\n * @public\n */\nexport class AwsS3UrlReader implements UrlReaderService {\n static factory: ReaderFactory = ({ config, treeResponseFactory }) => {\n const integrations = ScmIntegrations.fromConfig(config);\n const credsManager = DefaultAwsCredentialsManager.fromConfig(config);\n\n return integrations.awsS3.list().map(integration => {\n const reader = new AwsS3UrlReader(credsManager, integration, {\n treeResponseFactory,\n });\n const predicate = (url: URL) =>\n url.host.endsWith(integration.config.host);\n return { reader, predicate };\n });\n };\n\n private readonly credsManager: AwsCredentialsManager;\n private readonly integration: AwsS3Integration;\n private readonly deps: {\n treeResponseFactory: ReadTreeResponseFactory;\n };\n\n constructor(\n credsManager: AwsCredentialsManager,\n integration: AwsS3Integration,\n deps: {\n treeResponseFactory: ReadTreeResponseFactory;\n },\n ) {\n this.credsManager = credsManager;\n this.integration = integration;\n this.deps = deps;\n }\n\n /**\n * If accessKeyId and secretAccessKey are missing, the standard credentials provider chain will be used:\n * https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/auth/DefaultAWSCredentialsProviderChain.html\n */\n private static buildStaticCredentials(\n accessKeyId: string,\n secretAccessKey: string,\n ): AwsCredentialIdentityProvider {\n return async () => {\n return {\n accessKeyId,\n secretAccessKey,\n };\n };\n }\n\n private static async buildCredentials(\n credsManager: AwsCredentialsManager,\n region: string,\n integration?: AwsS3Integration,\n ): Promise<AwsCredentialIdentityProvider> {\n // Fall back to the default credential chain if neither account ID\n // nor explicit credentials are provided\n if (!integration) {\n return (await credsManager.getCredentialProvider()).sdkCredentialProvider;\n }\n\n const accessKeyId = integration.config.accessKeyId;\n const secretAccessKey = integration.config.secretAccessKey;\n const roleArn = integration.config.roleArn;\n\n if (accessKeyId && secretAccessKey) {\n const explicitCredentials = AwsS3UrlReader.buildStaticCredentials(\n accessKeyId,\n secretAccessKey,\n );\n if (roleArn) {\n return fromTemporaryCredentials({\n masterCredentials: explicitCredentials,\n params: {\n RoleSessionName: 'backstage-aws-s3-url-reader',\n RoleArn: roleArn,\n ExternalId: integration.config.externalId,\n },\n clientConfig: { region },\n });\n }\n return explicitCredentials;\n }\n\n if (roleArn) {\n let masterCredentials: AwsCredentialIdentityProvider;\n try {\n masterCredentials = (\n await credsManager.getCredentialProvider({ arn: roleArn })\n ).sdkCredentialProvider;\n } catch {\n // No account-specific config for this ARN; fall back to default credentials\n masterCredentials = (await credsManager.getCredentialProvider())\n .sdkCredentialProvider;\n }\n return fromTemporaryCredentials({\n masterCredentials,\n params: {\n RoleSessionName: 'backstage-aws-s3-url-reader',\n RoleArn: roleArn,\n ExternalId: integration.config.externalId,\n },\n clientConfig: { region },\n });\n }\n\n return (await credsManager.getCredentialProvider()).sdkCredentialProvider;\n }\n\n private async buildS3Client(\n credsManager: AwsCredentialsManager,\n region: string,\n integration: AwsS3Integration,\n ): Promise<S3Client> {\n const credentials = await AwsS3UrlReader.buildCredentials(\n credsManager,\n region,\n integration,\n );\n\n const s3 = new S3Client({\n customUserAgent: 'backstage-aws-s3-url-reader',\n region: region,\n credentials: credentials,\n endpoint: integration.config.endpoint,\n forcePathStyle: integration.config.s3ForcePathStyle,\n });\n return s3;\n }\n\n private async retrieveS3ObjectData(stream: Readable): Promise<Readable> {\n return new Promise((resolve, reject) => {\n try {\n const chunks: any[] = [];\n stream.on('data', chunk => chunks.push(chunk));\n stream.on('error', (e: Error) =>\n reject(new ForwardedError('Unable to read stream', e)),\n );\n stream.on('end', () => resolve(Readable.from(Buffer.concat(chunks))));\n } catch (e) {\n throw new ForwardedError('Unable to parse the response data', e);\n }\n });\n }\n\n async read(url: string): Promise<Buffer> {\n const response = await this.readUrl(url);\n return response.buffer();\n }\n\n async readUrl(\n url: string,\n options?: UrlReaderServiceReadUrlOptions,\n ): Promise<UrlReaderServiceReadUrlResponse> {\n const { etag, lastModifiedAfter } = options ?? {};\n\n try {\n const { path, bucket, region } = parseUrl(url, this.integration.config);\n const s3Client = await this.buildS3Client(\n this.credsManager,\n region,\n this.integration,\n );\n const abortController = new AbortController();\n\n const params: GetObjectCommandInput = {\n Bucket: bucket,\n Key: path,\n ...(etag && { IfNoneMatch: etag }),\n ...(lastModifiedAfter && {\n IfModifiedSince: lastModifiedAfter,\n }),\n };\n\n options?.signal?.addEventListener('abort', () => abortController.abort());\n const getObjectCommand = new GetObjectCommand(params);\n const response = await s3Client.send(getObjectCommand, {\n abortSignal: abortController.signal,\n });\n\n const s3ObjectData = await this.retrieveS3ObjectData(\n response.Body as Readable,\n );\n\n return ReadUrlResponseFactory.fromReadable(s3ObjectData, {\n etag: response.ETag,\n lastModifiedAt: response.LastModified,\n });\n } catch (e) {\n if (e.$metadata && e.$metadata.httpStatusCode === 304) {\n throw new NotModifiedError();\n }\n\n throw new ForwardedError('Could not retrieve file from S3', e);\n }\n }\n\n async readTree(\n url: string,\n options?: UrlReaderServiceReadTreeOptions,\n ): Promise<UrlReaderServiceReadTreeResponse> {\n try {\n const { path, bucket, region } = parseUrl(url, this.integration.config);\n const s3Client = await this.buildS3Client(\n this.credsManager,\n region,\n this.integration,\n );\n const abortController = new AbortController();\n const allObjects: String[] = [];\n const responses = [];\n let continuationToken: string | undefined;\n let output: ListObjectsV2CommandOutput;\n do {\n const listObjectsV2Command = new ListObjectsV2Command({\n Bucket: bucket,\n ContinuationToken: continuationToken,\n Prefix: path,\n });\n options?.signal?.addEventListener('abort', () =>\n abortController.abort(),\n );\n output = await s3Client.send(listObjectsV2Command, {\n abortSignal: abortController.signal,\n });\n if (output.Contents) {\n output.Contents.forEach(contents => {\n allObjects.push(contents.Key!);\n });\n }\n continuationToken = output.NextContinuationToken;\n } while (continuationToken);\n\n for (let i = 0; i < allObjects.length; i++) {\n if (hasDotPathSegments(String(allObjects[i]))) {\n continue;\n }\n const getObjectCommand = new GetObjectCommand({\n Bucket: bucket,\n Key: String(allObjects[i]),\n });\n const response = await s3Client.send(getObjectCommand);\n const s3ObjectData = await this.retrieveS3ObjectData(\n response.Body as Readable,\n );\n\n responses.push({\n data: s3ObjectData,\n path: relative(path, String(allObjects[i])),\n lastModifiedAt: response?.LastModified ?? undefined,\n });\n }\n\n return await this.deps.treeResponseFactory.fromReadableArray(responses);\n } catch (e) {\n throw new ForwardedError('Could not retrieve file tree from S3', e);\n }\n }\n\n async search(\n url: string,\n options?: UrlReaderServiceSearchOptions,\n ): Promise<UrlReaderServiceSearchResponse> {\n const { path } = parseUrl(url, this.integration.config);\n\n if (path.match(/[*?]/)) {\n throw new Error('Unsupported search pattern URL');\n }\n\n try {\n const data = await this.readUrl(url, options);\n\n return {\n files: [\n {\n url: url,\n content: data.buffer,\n lastModifiedAt: data.lastModifiedAt,\n },\n ],\n etag: data.etag ?? '',\n };\n } catch (e) {\n const error = toError(e);\n if (error.name === 'NotFoundError') {\n return {\n files: [],\n etag: '',\n };\n }\n throw error;\n }\n }\n\n toString() {\n const secretAccessKey = this.integration.config.secretAccessKey;\n return `awsS3{host=${this.integration.config.host},authed=${Boolean(\n secretAccessKey,\n )}}`;\n }\n}\n"],"names":["isUrlPathWithoutDotSegments","ScmIntegrations","DefaultAwsCredentialsManager","fromTemporaryCredentials","S3Client","ForwardedError","Readable","abortController","AbortController","GetObjectCommand","ReadUrlResponseFactory","NotModifiedError","ListObjectsV2Command","hasDotPathSegments","relative","toError"],"mappings":"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"}
@@ -2,7 +2,7 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
- var version = "0.17.9-next.1";
5
+ var version = "0.18.0";
6
6
  var packageinfo = {
7
7
  version: version};
8
8
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backstage/backend-defaults",
3
- "version": "0.17.9-next.1",
3
+ "version": "0.18.0",
4
4
  "description": "Backend defaults used by Backstage backend apps",
5
5
  "backstage": {
6
6
  "role": "node-library"
@@ -226,20 +226,20 @@
226
226
  "@aws-sdk/types": "^3.347.0",
227
227
  "@azure/identity": "^4.0.0",
228
228
  "@azure/storage-blob": "^12.5.0",
229
- "@backstage/backend-app-api": "1.7.4-next.1",
230
- "@backstage/backend-dev-utils": "0.1.7",
231
- "@backstage/backend-plugin-api": "1.10.1-next.1",
232
- "@backstage/cli-node": "0.3.4",
233
- "@backstage/config": "1.3.9-next.0",
234
- "@backstage/config-loader": "1.11.3-next.0",
235
- "@backstage/errors": "1.3.1",
236
- "@backstage/integration": "2.1.2-next.1",
237
- "@backstage/integration-aws-node": "0.2.2-next.0",
238
- "@backstage/plugin-auth-node": "0.7.5-next.1",
239
- "@backstage/plugin-events-node": "0.4.26-next.0",
240
- "@backstage/plugin-permission-common": "0.9.11-next.0",
241
- "@backstage/plugin-permission-node": "0.11.4-next.1",
242
- "@backstage/types": "1.2.2",
229
+ "@backstage/backend-app-api": "^1.7.4",
230
+ "@backstage/backend-dev-utils": "^0.1.7",
231
+ "@backstage/backend-plugin-api": "^1.10.1",
232
+ "@backstage/cli-node": "^0.3.4",
233
+ "@backstage/config": "^1.3.9",
234
+ "@backstage/config-loader": "^1.11.3",
235
+ "@backstage/errors": "^1.3.1",
236
+ "@backstage/integration": "^2.1.2",
237
+ "@backstage/integration-aws-node": "^0.2.2",
238
+ "@backstage/plugin-auth-node": "^0.7.6",
239
+ "@backstage/plugin-events-node": "^0.4.26",
240
+ "@backstage/plugin-permission-common": "^0.9.11",
241
+ "@backstage/plugin-permission-node": "^0.11.4",
242
+ "@backstage/types": "^1.2.2",
243
243
  "@google-cloud/storage": "^7.0.0",
244
244
  "@keyv/memcache": "^2.0.1",
245
245
  "@keyv/redis": "^4.0.1",
@@ -294,13 +294,14 @@
294
294
  },
295
295
  "devDependencies": {
296
296
  "@aws-sdk/util-stream-node": "^3.350.0",
297
- "@backstage/backend-test-utils": "1.11.7-next.1",
298
- "@backstage/cli": "0.36.6-next.1",
297
+ "@backstage/backend-test-utils": "^1.11.7",
298
+ "@backstage/cli": "^0.36.6",
299
299
  "@google-cloud/cloud-sql-connector": "^1.4.0",
300
300
  "@types/archiver": "^7.0.0",
301
301
  "@types/base64-stream": "^1.0.2",
302
302
  "@types/compression": "^1.7.5",
303
303
  "@types/concat-stream": "^2.0.0",
304
+ "@types/cookie": "^0.6.0",
304
305
  "@types/http-errors": "^2.0.0",
305
306
  "@types/is-glob": "^4.0.2",
306
307
  "@types/node-forge": "^1.3.0",