@backstage/backend-app-api 0.7.6-next.3 → 0.7.6
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 +69 -0
- package/alpha/package.json +1 -1
- package/config.d.ts +33 -0
- package/dist/alpha.cjs.js +1 -1
- package/dist/alpha.cjs.js.map +1 -1
- package/dist/index.cjs.js +1449 -1158
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +246 -85
- package/package.json +13 -11
- package/migrations/20240327104803_public_keys.js +0 -50
package/dist/index.cjs.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var configLoader = require('@backstage/config-loader');
|
|
4
|
+
var getPackages = require('@manypkg/get-packages');
|
|
3
5
|
var path = require('path');
|
|
4
6
|
var parseArgs = require('minimist');
|
|
5
7
|
var cliCommon = require('@backstage/cli-common');
|
|
6
|
-
var configLoader = require('@backstage/config-loader');
|
|
7
8
|
var config = require('@backstage/config');
|
|
8
|
-
var getPackages = require('@manypkg/get-packages');
|
|
9
9
|
var http = require('http');
|
|
10
10
|
var https = require('https');
|
|
11
11
|
var stoppableServer = require('stoppable');
|
|
@@ -19,24 +19,25 @@ var kebabCase = require('lodash/kebabCase');
|
|
|
19
19
|
var minimatch = require('minimatch');
|
|
20
20
|
var errors = require('@backstage/errors');
|
|
21
21
|
var crypto = require('crypto');
|
|
22
|
+
var backendPluginApi = require('@backstage/backend-plugin-api');
|
|
22
23
|
var winston = require('winston');
|
|
23
24
|
var tripleBeam = require('triple-beam');
|
|
24
|
-
var backendPluginApi = require('@backstage/backend-plugin-api');
|
|
25
25
|
var alpha = require('@backstage/backend-plugin-api/alpha');
|
|
26
|
-
var
|
|
27
|
-
var jose = require('jose');
|
|
28
|
-
var uuid = require('uuid');
|
|
26
|
+
var backendCommon = require('@backstage/backend-common');
|
|
29
27
|
var pluginAuthNode = require('@backstage/plugin-auth-node');
|
|
28
|
+
var pluginPermissionNode = require('@backstage/plugin-permission-node');
|
|
29
|
+
var jose = require('jose');
|
|
30
30
|
var types = require('@backstage/types');
|
|
31
|
-
var
|
|
32
|
-
var
|
|
31
|
+
var uuid = require('uuid');
|
|
32
|
+
var luxon = require('luxon');
|
|
33
|
+
var fs$1 = require('fs');
|
|
33
34
|
var cookie = require('cookie');
|
|
34
35
|
var Router = require('express-promise-router');
|
|
35
36
|
var pathToRegexp = require('path-to-regexp');
|
|
36
|
-
var pluginPermissionNode = require('@backstage/plugin-permission-node');
|
|
37
37
|
var express = require('express');
|
|
38
38
|
var trimEnd = require('lodash/trimEnd');
|
|
39
39
|
var backendTasks = require('@backstage/backend-tasks');
|
|
40
|
+
var fetch$1 = require('node-fetch');
|
|
40
41
|
|
|
41
42
|
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
|
|
42
43
|
|
|
@@ -72,6 +73,33 @@ var kebabCase__default = /*#__PURE__*/_interopDefaultCompat(kebabCase);
|
|
|
72
73
|
var Router__default = /*#__PURE__*/_interopDefaultCompat(Router);
|
|
73
74
|
var express__default = /*#__PURE__*/_interopDefaultCompat(express);
|
|
74
75
|
var trimEnd__default = /*#__PURE__*/_interopDefaultCompat(trimEnd);
|
|
76
|
+
var fetch__default = /*#__PURE__*/_interopDefaultCompat(fetch$1);
|
|
77
|
+
|
|
78
|
+
async function createConfigSecretEnumerator$1(options) {
|
|
79
|
+
const { logger, dir = process.cwd() } = options;
|
|
80
|
+
const { packages } = await getPackages.getPackages(dir);
|
|
81
|
+
const schema = options.schema ?? await configLoader.loadConfigSchema({
|
|
82
|
+
dependencies: packages.map((p) => p.packageJson.name)
|
|
83
|
+
});
|
|
84
|
+
return (config) => {
|
|
85
|
+
const [secretsData] = schema.process(
|
|
86
|
+
[{ data: config.getOptional() ?? {}, context: "schema-enumerator" }],
|
|
87
|
+
{
|
|
88
|
+
visibility: ["secret"],
|
|
89
|
+
ignoreSchemaErrors: true
|
|
90
|
+
}
|
|
91
|
+
);
|
|
92
|
+
const secrets = /* @__PURE__ */ new Set();
|
|
93
|
+
JSON.parse(
|
|
94
|
+
JSON.stringify(secretsData.data),
|
|
95
|
+
(_, v) => typeof v === "string" && secrets.add(v)
|
|
96
|
+
);
|
|
97
|
+
logger.info(
|
|
98
|
+
`Found ${secrets.size} new secrets in config that will be redacted`
|
|
99
|
+
);
|
|
100
|
+
return secrets;
|
|
101
|
+
};
|
|
102
|
+
}
|
|
75
103
|
|
|
76
104
|
class ObservableConfigProxy {
|
|
77
105
|
constructor(parent, parentKey) {
|
|
@@ -181,31 +209,7 @@ function isValidUrl(url) {
|
|
|
181
209
|
}
|
|
182
210
|
}
|
|
183
211
|
|
|
184
|
-
|
|
185
|
-
const { logger, dir = process.cwd() } = options;
|
|
186
|
-
const { packages } = await getPackages.getPackages(dir);
|
|
187
|
-
const schema = options.schema ?? await configLoader.loadConfigSchema({
|
|
188
|
-
dependencies: packages.map((p) => p.packageJson.name)
|
|
189
|
-
});
|
|
190
|
-
return (config) => {
|
|
191
|
-
const [secretsData] = schema.process(
|
|
192
|
-
[{ data: config.getOptional() ?? {}, context: "schema-enumerator" }],
|
|
193
|
-
{
|
|
194
|
-
visibility: ["secret"],
|
|
195
|
-
ignoreSchemaErrors: true
|
|
196
|
-
}
|
|
197
|
-
);
|
|
198
|
-
const secrets = /* @__PURE__ */ new Set();
|
|
199
|
-
JSON.parse(
|
|
200
|
-
JSON.stringify(secretsData.data),
|
|
201
|
-
(_, v) => typeof v === "string" && secrets.add(v)
|
|
202
|
-
);
|
|
203
|
-
logger.info(
|
|
204
|
-
`Found ${secrets.size} new secrets in config that will be redacted`
|
|
205
|
-
);
|
|
206
|
-
return secrets;
|
|
207
|
-
};
|
|
208
|
-
}
|
|
212
|
+
const createConfigSecretEnumerator = createConfigSecretEnumerator$1;
|
|
209
213
|
async function loadBackendConfig(options) {
|
|
210
214
|
const args = parseArgs__default.default(options.argv);
|
|
211
215
|
const configTargets = [args.config ?? []].flat().map((arg) => isValidUrl(arg) ? { url: arg } : { path: path.resolve(arg) });
|
|
@@ -251,7 +255,7 @@ async function loadBackendConfig(options) {
|
|
|
251
255
|
|
|
252
256
|
const DEFAULT_PORT = 7007;
|
|
253
257
|
const DEFAULT_HOST = "";
|
|
254
|
-
function readHttpServerOptions(config) {
|
|
258
|
+
function readHttpServerOptions$1(config) {
|
|
255
259
|
return {
|
|
256
260
|
listen: readHttpListenOptions(config),
|
|
257
261
|
https: readHttpsOptions(config)
|
|
@@ -426,7 +430,7 @@ async function generateCertificate(hostname) {
|
|
|
426
430
|
);
|
|
427
431
|
}
|
|
428
432
|
|
|
429
|
-
async function createHttpServer(listener, options, deps) {
|
|
433
|
+
async function createHttpServer$1(listener, options, deps) {
|
|
430
434
|
const server = await createServer(listener, options, deps);
|
|
431
435
|
const stopper = stoppableServer__default.default(server, 0);
|
|
432
436
|
const stopServer = stopper.stop.bind(stopper);
|
|
@@ -481,7 +485,7 @@ async function createServer(listener, options, deps) {
|
|
|
481
485
|
return http__namespace.createServer(listener);
|
|
482
486
|
}
|
|
483
487
|
|
|
484
|
-
function readHelmetOptions(config) {
|
|
488
|
+
function readHelmetOptions$1(config) {
|
|
485
489
|
const cspOptions = readCspDirectives(config);
|
|
486
490
|
return {
|
|
487
491
|
contentSecurityPolicy: {
|
|
@@ -530,7 +534,7 @@ function applyCspDirectives(directives) {
|
|
|
530
534
|
return result;
|
|
531
535
|
}
|
|
532
536
|
|
|
533
|
-
function readCorsOptions(config) {
|
|
537
|
+
function readCorsOptions$1(config) {
|
|
534
538
|
const cc = config?.getOptionalConfig("cors");
|
|
535
539
|
if (!cc) {
|
|
536
540
|
return { origin: false };
|
|
@@ -596,7 +600,7 @@ function applyInternalErrorFilter(error, logger) {
|
|
|
596
600
|
return error;
|
|
597
601
|
}
|
|
598
602
|
|
|
599
|
-
class MiddlewareFactory {
|
|
603
|
+
let MiddlewareFactory$1 = class MiddlewareFactory {
|
|
600
604
|
#config;
|
|
601
605
|
#logger;
|
|
602
606
|
/**
|
|
@@ -671,7 +675,7 @@ class MiddlewareFactory {
|
|
|
671
675
|
* @returns An Express request handler
|
|
672
676
|
*/
|
|
673
677
|
helmet() {
|
|
674
|
-
return helmet__default.default(readHelmetOptions(this.#config.getOptionalConfig("backend")));
|
|
678
|
+
return helmet__default.default(readHelmetOptions$1(this.#config.getOptionalConfig("backend")));
|
|
675
679
|
}
|
|
676
680
|
/**
|
|
677
681
|
* Returns a middleware that implements the cors library.
|
|
@@ -686,7 +690,7 @@ class MiddlewareFactory {
|
|
|
686
690
|
* @returns An Express request handler
|
|
687
691
|
*/
|
|
688
692
|
cors() {
|
|
689
|
-
return cors__default.default(readCorsOptions(this.#config.getOptionalConfig("backend")));
|
|
693
|
+
return cors__default.default(readCorsOptions$1(this.#config.getOptionalConfig("backend")));
|
|
690
694
|
}
|
|
691
695
|
/**
|
|
692
696
|
* Express middleware to handle errors during request processing.
|
|
@@ -731,7 +735,7 @@ class MiddlewareFactory {
|
|
|
731
735
|
res.status(statusCode).json(body);
|
|
732
736
|
};
|
|
733
737
|
}
|
|
734
|
-
}
|
|
738
|
+
};
|
|
735
739
|
function getStatusCode(error) {
|
|
736
740
|
const knownStatusCodeFields = ["statusCode", "status"];
|
|
737
741
|
for (const field of knownStatusCodeFields) {
|
|
@@ -762,11 +766,17 @@ function getStatusCode(error) {
|
|
|
762
766
|
return 500;
|
|
763
767
|
}
|
|
764
768
|
|
|
769
|
+
const readHttpServerOptions = readHttpServerOptions$1;
|
|
770
|
+
const createHttpServer = createHttpServer$1;
|
|
771
|
+
const readCorsOptions = readCorsOptions$1;
|
|
772
|
+
const readHelmetOptions = readHelmetOptions$1;
|
|
773
|
+
const MiddlewareFactory = MiddlewareFactory$1;
|
|
774
|
+
|
|
765
775
|
const escapeRegExp = (text) => {
|
|
766
776
|
return text.replace(/[.*+?^${}(\)|[\]\\]/g, "\\$&");
|
|
767
777
|
};
|
|
768
778
|
|
|
769
|
-
class WinstonLogger {
|
|
779
|
+
let WinstonLogger$1 = class WinstonLogger {
|
|
770
780
|
#winston;
|
|
771
781
|
#addRedactions;
|
|
772
782
|
/**
|
|
@@ -870,6 +880,69 @@ class WinstonLogger {
|
|
|
870
880
|
addRedactions(redactions) {
|
|
871
881
|
this.#addRedactions?.(redactions);
|
|
872
882
|
}
|
|
883
|
+
};
|
|
884
|
+
|
|
885
|
+
const rootLoggerServiceFactory$1 = backendPluginApi.createServiceFactory({
|
|
886
|
+
service: backendPluginApi.coreServices.rootLogger,
|
|
887
|
+
deps: {
|
|
888
|
+
config: backendPluginApi.coreServices.rootConfig
|
|
889
|
+
},
|
|
890
|
+
async factory({ config }) {
|
|
891
|
+
const logger = WinstonLogger$1.create({
|
|
892
|
+
meta: {
|
|
893
|
+
service: "backstage"
|
|
894
|
+
},
|
|
895
|
+
level: process.env.LOG_LEVEL || "info",
|
|
896
|
+
format: process.env.NODE_ENV === "production" ? winston.format.json() : WinstonLogger$1.colorFormat(),
|
|
897
|
+
transports: [new winston.transports.Console()]
|
|
898
|
+
});
|
|
899
|
+
const secretEnumerator = await createConfigSecretEnumerator$1({ logger });
|
|
900
|
+
logger.addRedactions(secretEnumerator(config));
|
|
901
|
+
config.subscribe?.(() => logger.addRedactions(secretEnumerator(config)));
|
|
902
|
+
return logger;
|
|
903
|
+
}
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
class WinstonLogger {
|
|
907
|
+
constructor(impl) {
|
|
908
|
+
this.impl = impl;
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* Creates a {@link WinstonLogger} instance.
|
|
912
|
+
*/
|
|
913
|
+
static create(options) {
|
|
914
|
+
return new WinstonLogger(WinstonLogger$1.create(options));
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* Creates a winston log formatter for redacting secrets.
|
|
918
|
+
*/
|
|
919
|
+
static redacter() {
|
|
920
|
+
return WinstonLogger$1.redacter();
|
|
921
|
+
}
|
|
922
|
+
/**
|
|
923
|
+
* Creates a pretty printed winston log formatter.
|
|
924
|
+
*/
|
|
925
|
+
static colorFormat() {
|
|
926
|
+
return WinstonLogger$1.colorFormat();
|
|
927
|
+
}
|
|
928
|
+
error(message, meta) {
|
|
929
|
+
this.impl.error(message, meta);
|
|
930
|
+
}
|
|
931
|
+
warn(message, meta) {
|
|
932
|
+
this.impl.warn(message, meta);
|
|
933
|
+
}
|
|
934
|
+
info(message, meta) {
|
|
935
|
+
this.impl.info(message, meta);
|
|
936
|
+
}
|
|
937
|
+
debug(message, meta) {
|
|
938
|
+
this.impl.debug(message, meta);
|
|
939
|
+
}
|
|
940
|
+
child(meta) {
|
|
941
|
+
return this.impl.child(meta);
|
|
942
|
+
}
|
|
943
|
+
addRedactions(redactions) {
|
|
944
|
+
this.impl.addRedactions(redactions);
|
|
945
|
+
}
|
|
873
946
|
}
|
|
874
947
|
|
|
875
948
|
class Node {
|
|
@@ -1633,102 +1706,417 @@ function createSpecializedBackend(options) {
|
|
|
1633
1706
|
return new BackstageBackend(services);
|
|
1634
1707
|
}
|
|
1635
1708
|
|
|
1636
|
-
const
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
})
|
|
1647
|
-
|
|
1648
|
-
class DatabaseKeyStore {
|
|
1649
|
-
constructor(client, logger) {
|
|
1650
|
-
this.client = client;
|
|
1651
|
-
this.logger = logger;
|
|
1652
|
-
}
|
|
1653
|
-
static async create(options) {
|
|
1654
|
-
const { database, logger } = options;
|
|
1655
|
-
const client = await database.getClient();
|
|
1656
|
-
if (!database.migrations?.skip) {
|
|
1657
|
-
await applyDatabaseMigrations(client);
|
|
1658
|
-
}
|
|
1659
|
-
return new DatabaseKeyStore(client, logger);
|
|
1660
|
-
}
|
|
1661
|
-
async addKey(options) {
|
|
1662
|
-
await this.client(TABLE).insert({
|
|
1663
|
-
id: options.key.kid,
|
|
1664
|
-
key: JSON.stringify(options.key),
|
|
1665
|
-
expires_at: options.expiresAt.toISOString()
|
|
1666
|
-
});
|
|
1709
|
+
const cacheServiceFactory = backendPluginApi.createServiceFactory({
|
|
1710
|
+
service: backendPluginApi.coreServices.cache,
|
|
1711
|
+
deps: {
|
|
1712
|
+
config: backendPluginApi.coreServices.rootConfig,
|
|
1713
|
+
logger: backendPluginApi.coreServices.rootLogger,
|
|
1714
|
+
plugin: backendPluginApi.coreServices.pluginMetadata
|
|
1715
|
+
},
|
|
1716
|
+
async createRootContext({ config, logger }) {
|
|
1717
|
+
return backendCommon.CacheManager.fromConfig(config, { logger });
|
|
1718
|
+
},
|
|
1719
|
+
async factory({ plugin }, manager) {
|
|
1720
|
+
return manager.forPlugin(plugin.getId()).getClient();
|
|
1667
1721
|
}
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
expiredKeys.push(key);
|
|
1680
|
-
} else {
|
|
1681
|
-
validKeys.push(key);
|
|
1682
|
-
}
|
|
1683
|
-
}
|
|
1684
|
-
if (expiredKeys.length > 0) {
|
|
1685
|
-
const kids = expiredKeys.map(({ key }) => key.kid);
|
|
1686
|
-
this.logger.info(
|
|
1687
|
-
`Removing expired plugin service keys, '${kids.join("', '")}'`
|
|
1688
|
-
);
|
|
1689
|
-
this.client(TABLE).delete().whereIn("id", kids).catch((error) => {
|
|
1690
|
-
this.logger.error(
|
|
1691
|
-
"Failed to remove expired plugin service keys",
|
|
1692
|
-
error
|
|
1693
|
-
);
|
|
1722
|
+
});
|
|
1723
|
+
|
|
1724
|
+
const rootConfigServiceFactory = backendPluginApi.createServiceFactory(
|
|
1725
|
+
(options) => ({
|
|
1726
|
+
service: backendPluginApi.coreServices.rootConfig,
|
|
1727
|
+
deps: {},
|
|
1728
|
+
async factory() {
|
|
1729
|
+
const source = configLoader.ConfigSources.default({
|
|
1730
|
+
argv: options?.argv,
|
|
1731
|
+
remote: options?.remote,
|
|
1732
|
+
watch: options?.watch
|
|
1694
1733
|
});
|
|
1734
|
+
console.log(`Loading config from ${source}`);
|
|
1735
|
+
return await configLoader.ConfigSources.toConfig(source);
|
|
1695
1736
|
}
|
|
1696
|
-
|
|
1737
|
+
})
|
|
1738
|
+
);
|
|
1739
|
+
|
|
1740
|
+
const databaseServiceFactory = backendPluginApi.createServiceFactory({
|
|
1741
|
+
service: backendPluginApi.coreServices.database,
|
|
1742
|
+
deps: {
|
|
1743
|
+
config: backendPluginApi.coreServices.rootConfig,
|
|
1744
|
+
lifecycle: backendPluginApi.coreServices.lifecycle,
|
|
1745
|
+
pluginMetadata: backendPluginApi.coreServices.pluginMetadata
|
|
1746
|
+
},
|
|
1747
|
+
async createRootContext({ config: config$1 }) {
|
|
1748
|
+
return config$1.getOptional("backend.database") ? backendCommon.DatabaseManager.fromConfig(config$1) : backendCommon.DatabaseManager.fromConfig(
|
|
1749
|
+
new config.ConfigReader({
|
|
1750
|
+
backend: {
|
|
1751
|
+
database: { client: "better-sqlite3", connection: ":memory:" }
|
|
1752
|
+
}
|
|
1753
|
+
})
|
|
1754
|
+
);
|
|
1755
|
+
},
|
|
1756
|
+
async factory({ pluginMetadata, lifecycle }, databaseManager) {
|
|
1757
|
+
return databaseManager.forPlugin(pluginMetadata.getId(), {
|
|
1758
|
+
pluginMetadata,
|
|
1759
|
+
lifecycle
|
|
1760
|
+
});
|
|
1697
1761
|
}
|
|
1698
|
-
}
|
|
1762
|
+
});
|
|
1699
1763
|
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1764
|
+
let HostDiscovery$1 = class HostDiscovery {
|
|
1765
|
+
constructor(internalBaseUrl, externalBaseUrl, discoveryConfig) {
|
|
1766
|
+
this.internalBaseUrl = internalBaseUrl;
|
|
1767
|
+
this.externalBaseUrl = externalBaseUrl;
|
|
1768
|
+
this.discoveryConfig = discoveryConfig;
|
|
1769
|
+
}
|
|
1770
|
+
/**
|
|
1771
|
+
* Creates a new HostDiscovery discovery instance by reading
|
|
1772
|
+
* from the `backend` config section, specifically the `.baseUrl` for
|
|
1773
|
+
* discovering the external URL, and the `.listen` and `.https` config
|
|
1774
|
+
* for the internal one.
|
|
1775
|
+
*
|
|
1776
|
+
* Can be overridden in config by providing a target and corresponding plugins in `discovery.endpoints`.
|
|
1777
|
+
* eg.
|
|
1778
|
+
* ```yaml
|
|
1779
|
+
* discovery:
|
|
1780
|
+
* endpoints:
|
|
1781
|
+
* - target: https://internal.example.com/internal-catalog
|
|
1782
|
+
* plugins: [catalog]
|
|
1783
|
+
* - target: https://internal.example.com/secure/api/{{pluginId}}
|
|
1784
|
+
* plugins: [auth, permission]
|
|
1785
|
+
* - target:
|
|
1786
|
+
* internal: https://internal.example.com/search
|
|
1787
|
+
* external: https://example.com/search
|
|
1788
|
+
* plugins: [search]
|
|
1789
|
+
* ```
|
|
1790
|
+
*
|
|
1791
|
+
* The basePath defaults to `/api`, meaning the default full internal
|
|
1792
|
+
* path for the `catalog` plugin will be `http://localhost:7007/api/catalog`.
|
|
1793
|
+
*/
|
|
1794
|
+
static fromConfig(config, options) {
|
|
1795
|
+
const basePath = options?.basePath ?? "/api";
|
|
1796
|
+
const externalBaseUrl = config.getString("backend.baseUrl").replace(/\/+$/, "");
|
|
1797
|
+
const {
|
|
1798
|
+
listen: { host: listenHost = "::", port: listenPort }
|
|
1799
|
+
} = readHttpServerOptions$1(config.getConfig("backend"));
|
|
1800
|
+
const protocol = config.has("backend.https") ? "https" : "http";
|
|
1801
|
+
let host = listenHost;
|
|
1802
|
+
if (host === "::" || host === "") {
|
|
1803
|
+
host = "localhost";
|
|
1804
|
+
} else if (host === "0.0.0.0") {
|
|
1805
|
+
host = "127.0.0.1";
|
|
1806
|
+
}
|
|
1807
|
+
if (host.includes(":")) {
|
|
1808
|
+
host = `[${host}]`;
|
|
1809
|
+
}
|
|
1810
|
+
const internalBaseUrl = `${protocol}://${host}:${listenPort}`;
|
|
1811
|
+
return new HostDiscovery(
|
|
1812
|
+
internalBaseUrl + basePath,
|
|
1813
|
+
externalBaseUrl + basePath,
|
|
1814
|
+
config.getOptionalConfig("discovery")
|
|
1815
|
+
);
|
|
1816
|
+
}
|
|
1817
|
+
getTargetFromConfig(pluginId, type) {
|
|
1818
|
+
const endpoints = this.discoveryConfig?.getOptionalConfigArray("endpoints");
|
|
1819
|
+
const target = endpoints?.find((endpoint) => endpoint.getStringArray("plugins").includes(pluginId))?.get("target");
|
|
1820
|
+
if (!target) {
|
|
1821
|
+
const baseUrl = type === "external" ? this.externalBaseUrl : this.internalBaseUrl;
|
|
1822
|
+
return `${baseUrl}/${encodeURIComponent(pluginId)}`;
|
|
1823
|
+
}
|
|
1824
|
+
if (typeof target === "string") {
|
|
1825
|
+
return target.replace(
|
|
1826
|
+
/\{\{\s*pluginId\s*\}\}/g,
|
|
1827
|
+
encodeURIComponent(pluginId)
|
|
1828
|
+
);
|
|
1829
|
+
}
|
|
1830
|
+
return target[type].replace(
|
|
1831
|
+
/\{\{\s*pluginId\s*\}\}/g,
|
|
1832
|
+
encodeURIComponent(pluginId)
|
|
1833
|
+
);
|
|
1834
|
+
}
|
|
1835
|
+
async getBaseUrl(pluginId) {
|
|
1836
|
+
return this.getTargetFromConfig(pluginId, "internal");
|
|
1837
|
+
}
|
|
1838
|
+
async getExternalBaseUrl(pluginId) {
|
|
1839
|
+
return this.getTargetFromConfig(pluginId, "external");
|
|
1840
|
+
}
|
|
1841
|
+
};
|
|
1842
|
+
|
|
1843
|
+
backendPluginApi.createServiceFactory({
|
|
1844
|
+
service: backendPluginApi.coreServices.discovery,
|
|
1845
|
+
deps: {
|
|
1846
|
+
config: backendPluginApi.coreServices.rootConfig
|
|
1847
|
+
},
|
|
1848
|
+
async factory({ config }) {
|
|
1849
|
+
return HostDiscovery$1.fromConfig(config);
|
|
1850
|
+
}
|
|
1851
|
+
});
|
|
1852
|
+
|
|
1853
|
+
class HostDiscovery {
|
|
1854
|
+
constructor(impl) {
|
|
1855
|
+
this.impl = impl;
|
|
1856
|
+
}
|
|
1857
|
+
/**
|
|
1858
|
+
* Creates a new HostDiscovery discovery instance by reading
|
|
1859
|
+
* from the `backend` config section, specifically the `.baseUrl` for
|
|
1860
|
+
* discovering the external URL, and the `.listen` and `.https` config
|
|
1861
|
+
* for the internal one.
|
|
1862
|
+
*
|
|
1863
|
+
* Can be overridden in config by providing a target and corresponding plugins in `discovery.endpoints`.
|
|
1864
|
+
* eg.
|
|
1865
|
+
* ```yaml
|
|
1866
|
+
* discovery:
|
|
1867
|
+
* endpoints:
|
|
1868
|
+
* - target: https://internal.example.com/internal-catalog
|
|
1869
|
+
* plugins: [catalog]
|
|
1870
|
+
* - target: https://internal.example.com/secure/api/{{pluginId}}
|
|
1871
|
+
* plugins: [auth, permission]
|
|
1872
|
+
* - target:
|
|
1873
|
+
* internal: https://internal.example.com/search
|
|
1874
|
+
* external: https://example.com/search
|
|
1875
|
+
* plugins: [search]
|
|
1876
|
+
* ```
|
|
1877
|
+
*
|
|
1878
|
+
* The basePath defaults to `/api`, meaning the default full internal
|
|
1879
|
+
* path for the `catalog` plugin will be `http://localhost:7007/api/catalog`.
|
|
1880
|
+
*/
|
|
1881
|
+
static fromConfig(config, options) {
|
|
1882
|
+
return new HostDiscovery(HostDiscovery$1.fromConfig(config, options));
|
|
1883
|
+
}
|
|
1884
|
+
async getBaseUrl(pluginId) {
|
|
1885
|
+
return this.impl.getBaseUrl(pluginId);
|
|
1886
|
+
}
|
|
1887
|
+
async getExternalBaseUrl(pluginId) {
|
|
1888
|
+
return this.impl.getExternalBaseUrl(pluginId);
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
const discoveryServiceFactory = backendPluginApi.createServiceFactory({
|
|
1893
|
+
service: backendPluginApi.coreServices.discovery,
|
|
1894
|
+
deps: {
|
|
1895
|
+
config: backendPluginApi.coreServices.rootConfig
|
|
1896
|
+
},
|
|
1897
|
+
async factory({ config }) {
|
|
1898
|
+
return HostDiscovery.fromConfig(config);
|
|
1899
|
+
}
|
|
1900
|
+
});
|
|
1901
|
+
|
|
1902
|
+
const identityServiceFactory = backendPluginApi.createServiceFactory(
|
|
1903
|
+
(options) => ({
|
|
1904
|
+
service: backendPluginApi.coreServices.identity,
|
|
1905
|
+
deps: {
|
|
1906
|
+
discovery: backendPluginApi.coreServices.discovery
|
|
1907
|
+
},
|
|
1908
|
+
async factory({ discovery }) {
|
|
1909
|
+
return pluginAuthNode.DefaultIdentityClient.create({ discovery, ...options });
|
|
1910
|
+
}
|
|
1911
|
+
})
|
|
1912
|
+
);
|
|
1913
|
+
|
|
1914
|
+
class BackendPluginLifecycleImpl {
|
|
1915
|
+
constructor(logger, rootLifecycle, pluginMetadata) {
|
|
1916
|
+
this.logger = logger;
|
|
1917
|
+
this.rootLifecycle = rootLifecycle;
|
|
1918
|
+
this.pluginMetadata = pluginMetadata;
|
|
1919
|
+
}
|
|
1920
|
+
#hasStarted = false;
|
|
1921
|
+
#startupTasks = [];
|
|
1922
|
+
addStartupHook(hook, options) {
|
|
1923
|
+
if (this.#hasStarted) {
|
|
1924
|
+
throw new Error("Attempted to add startup hook after startup");
|
|
1925
|
+
}
|
|
1926
|
+
this.#startupTasks.push({ hook, options });
|
|
1927
|
+
}
|
|
1928
|
+
async startup() {
|
|
1929
|
+
if (this.#hasStarted) {
|
|
1930
|
+
return;
|
|
1931
|
+
}
|
|
1932
|
+
this.#hasStarted = true;
|
|
1933
|
+
this.logger.debug(
|
|
1934
|
+
`Running ${this.#startupTasks.length} plugin startup tasks...`
|
|
1935
|
+
);
|
|
1936
|
+
await Promise.all(
|
|
1937
|
+
this.#startupTasks.map(async ({ hook, options }) => {
|
|
1938
|
+
const logger = options?.logger ?? this.logger;
|
|
1939
|
+
try {
|
|
1940
|
+
await hook();
|
|
1941
|
+
logger.debug(`Plugin startup hook succeeded`);
|
|
1942
|
+
} catch (error) {
|
|
1943
|
+
logger.error(`Plugin startup hook failed, ${error}`);
|
|
1944
|
+
}
|
|
1945
|
+
})
|
|
1946
|
+
);
|
|
1947
|
+
}
|
|
1948
|
+
addShutdownHook(hook, options) {
|
|
1949
|
+
const plugin = this.pluginMetadata.getId();
|
|
1950
|
+
this.rootLifecycle.addShutdownHook(hook, {
|
|
1951
|
+
logger: options?.logger?.child({ plugin }) ?? this.logger
|
|
1952
|
+
});
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
const lifecycleServiceFactory = backendPluginApi.createServiceFactory({
|
|
1956
|
+
service: backendPluginApi.coreServices.lifecycle,
|
|
1957
|
+
deps: {
|
|
1958
|
+
logger: backendPluginApi.coreServices.logger,
|
|
1959
|
+
rootLifecycle: backendPluginApi.coreServices.rootLifecycle,
|
|
1960
|
+
pluginMetadata: backendPluginApi.coreServices.pluginMetadata
|
|
1961
|
+
},
|
|
1962
|
+
async factory({ rootLifecycle, logger, pluginMetadata }) {
|
|
1963
|
+
return new BackendPluginLifecycleImpl(
|
|
1964
|
+
logger,
|
|
1965
|
+
rootLifecycle,
|
|
1966
|
+
pluginMetadata
|
|
1967
|
+
);
|
|
1968
|
+
}
|
|
1969
|
+
});
|
|
1970
|
+
|
|
1971
|
+
const permissionsServiceFactory = backendPluginApi.createServiceFactory({
|
|
1972
|
+
service: backendPluginApi.coreServices.permissions,
|
|
1973
|
+
deps: {
|
|
1974
|
+
auth: backendPluginApi.coreServices.auth,
|
|
1975
|
+
config: backendPluginApi.coreServices.rootConfig,
|
|
1976
|
+
discovery: backendPluginApi.coreServices.discovery,
|
|
1977
|
+
tokenManager: backendPluginApi.coreServices.tokenManager
|
|
1978
|
+
},
|
|
1979
|
+
async factory({ auth, config, discovery, tokenManager }) {
|
|
1980
|
+
return pluginPermissionNode.ServerPermissionClient.fromConfig(config, {
|
|
1981
|
+
auth,
|
|
1982
|
+
discovery,
|
|
1983
|
+
tokenManager
|
|
1984
|
+
});
|
|
1985
|
+
}
|
|
1986
|
+
});
|
|
1987
|
+
|
|
1988
|
+
class BackendLifecycleImpl {
|
|
1989
|
+
constructor(logger) {
|
|
1990
|
+
this.logger = logger;
|
|
1991
|
+
}
|
|
1992
|
+
#hasStarted = false;
|
|
1993
|
+
#startupTasks = [];
|
|
1994
|
+
addStartupHook(hook, options) {
|
|
1995
|
+
if (this.#hasStarted) {
|
|
1996
|
+
throw new Error("Attempted to add startup hook after startup");
|
|
1997
|
+
}
|
|
1998
|
+
this.#startupTasks.push({ hook, options });
|
|
1999
|
+
}
|
|
2000
|
+
async startup() {
|
|
2001
|
+
if (this.#hasStarted) {
|
|
2002
|
+
return;
|
|
2003
|
+
}
|
|
2004
|
+
this.#hasStarted = true;
|
|
2005
|
+
this.logger.debug(`Running ${this.#startupTasks.length} startup tasks...`);
|
|
2006
|
+
await Promise.all(
|
|
2007
|
+
this.#startupTasks.map(async ({ hook, options }) => {
|
|
2008
|
+
const logger = options?.logger ?? this.logger;
|
|
2009
|
+
try {
|
|
2010
|
+
await hook();
|
|
2011
|
+
logger.debug(`Startup hook succeeded`);
|
|
2012
|
+
} catch (error) {
|
|
2013
|
+
logger.error(`Startup hook failed, ${error}`);
|
|
2014
|
+
}
|
|
2015
|
+
})
|
|
2016
|
+
);
|
|
2017
|
+
}
|
|
2018
|
+
#hasShutdown = false;
|
|
2019
|
+
#shutdownTasks = [];
|
|
2020
|
+
addShutdownHook(hook, options) {
|
|
2021
|
+
if (this.#hasShutdown) {
|
|
2022
|
+
throw new Error("Attempted to add shutdown hook after shutdown");
|
|
2023
|
+
}
|
|
2024
|
+
this.#shutdownTasks.push({ hook, options });
|
|
2025
|
+
}
|
|
2026
|
+
async shutdown() {
|
|
2027
|
+
if (this.#hasShutdown) {
|
|
2028
|
+
return;
|
|
2029
|
+
}
|
|
2030
|
+
this.#hasShutdown = true;
|
|
2031
|
+
this.logger.debug(
|
|
2032
|
+
`Running ${this.#shutdownTasks.length} shutdown tasks...`
|
|
2033
|
+
);
|
|
2034
|
+
await Promise.all(
|
|
2035
|
+
this.#shutdownTasks.map(async ({ hook, options }) => {
|
|
2036
|
+
const logger = options?.logger ?? this.logger;
|
|
2037
|
+
try {
|
|
2038
|
+
await hook();
|
|
2039
|
+
logger.debug(`Shutdown hook succeeded`);
|
|
2040
|
+
} catch (error) {
|
|
2041
|
+
logger.error(`Shutdown hook failed, ${error}`);
|
|
2042
|
+
}
|
|
2043
|
+
})
|
|
2044
|
+
);
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
const rootLifecycleServiceFactory = backendPluginApi.createServiceFactory({
|
|
2048
|
+
service: backendPluginApi.coreServices.rootLifecycle,
|
|
2049
|
+
deps: {
|
|
2050
|
+
logger: backendPluginApi.coreServices.rootLogger
|
|
2051
|
+
},
|
|
2052
|
+
async factory({ logger }) {
|
|
2053
|
+
return new BackendLifecycleImpl(logger);
|
|
2054
|
+
}
|
|
2055
|
+
});
|
|
2056
|
+
|
|
2057
|
+
const tokenManagerServiceFactory = backendPluginApi.createServiceFactory({
|
|
2058
|
+
service: backendPluginApi.coreServices.tokenManager,
|
|
2059
|
+
deps: {
|
|
2060
|
+
config: backendPluginApi.coreServices.rootConfig,
|
|
2061
|
+
logger: backendPluginApi.coreServices.rootLogger
|
|
2062
|
+
},
|
|
2063
|
+
createRootContext({ config, logger }) {
|
|
2064
|
+
return backendCommon.ServerTokenManager.fromConfig(config, {
|
|
2065
|
+
logger,
|
|
2066
|
+
allowDisabledTokenManager: true
|
|
2067
|
+
});
|
|
2068
|
+
},
|
|
2069
|
+
async factory(_deps, tokenManager) {
|
|
2070
|
+
return tokenManager;
|
|
2071
|
+
}
|
|
2072
|
+
});
|
|
2073
|
+
|
|
2074
|
+
const urlReaderServiceFactory = backendPluginApi.createServiceFactory({
|
|
2075
|
+
service: backendPluginApi.coreServices.urlReader,
|
|
2076
|
+
deps: {
|
|
2077
|
+
config: backendPluginApi.coreServices.rootConfig,
|
|
2078
|
+
logger: backendPluginApi.coreServices.logger
|
|
2079
|
+
},
|
|
2080
|
+
async factory({ config, logger }) {
|
|
2081
|
+
return backendCommon.UrlReaders.default({
|
|
2082
|
+
config,
|
|
2083
|
+
logger
|
|
2084
|
+
});
|
|
2085
|
+
}
|
|
2086
|
+
});
|
|
2087
|
+
|
|
2088
|
+
function createCredentialsWithServicePrincipal(sub, token, accessRestrictions) {
|
|
2089
|
+
return {
|
|
2090
|
+
$$type: "@backstage/BackstageCredentials",
|
|
2091
|
+
version: "v1",
|
|
2092
|
+
token,
|
|
2093
|
+
principal: {
|
|
2094
|
+
type: "service",
|
|
2095
|
+
subject: sub,
|
|
2096
|
+
accessRestrictions
|
|
2097
|
+
}
|
|
2098
|
+
};
|
|
2099
|
+
}
|
|
2100
|
+
function createCredentialsWithUserPrincipal(sub, token, expiresAt) {
|
|
2101
|
+
return {
|
|
2102
|
+
$$type: "@backstage/BackstageCredentials",
|
|
2103
|
+
version: "v1",
|
|
2104
|
+
token,
|
|
2105
|
+
expiresAt,
|
|
2106
|
+
principal: {
|
|
2107
|
+
type: "user",
|
|
2108
|
+
userEntityRef: sub
|
|
2109
|
+
}
|
|
2110
|
+
};
|
|
2111
|
+
}
|
|
2112
|
+
function createCredentialsWithNonePrincipal() {
|
|
2113
|
+
return {
|
|
2114
|
+
$$type: "@backstage/BackstageCredentials",
|
|
2115
|
+
version: "v1",
|
|
2116
|
+
principal: {
|
|
2117
|
+
type: "none"
|
|
2118
|
+
}
|
|
2119
|
+
};
|
|
1732
2120
|
}
|
|
1733
2121
|
function toInternalBackstageCredentials(credentials) {
|
|
1734
2122
|
if (credentials.$$type !== "@backstage/BackstageCredentials") {
|
|
@@ -1744,17 +2132,16 @@ function toInternalBackstageCredentials(credentials) {
|
|
|
1744
2132
|
}
|
|
1745
2133
|
|
|
1746
2134
|
class DefaultAuthService {
|
|
1747
|
-
constructor(userTokenHandler, pluginTokenHandler, externalTokenHandler, tokenManager, pluginId, disableDefaultAuthPolicy,
|
|
2135
|
+
constructor(userTokenHandler, pluginTokenHandler, externalTokenHandler, tokenManager, pluginId, disableDefaultAuthPolicy, pluginKeySource) {
|
|
1748
2136
|
this.userTokenHandler = userTokenHandler;
|
|
1749
2137
|
this.pluginTokenHandler = pluginTokenHandler;
|
|
1750
2138
|
this.externalTokenHandler = externalTokenHandler;
|
|
1751
2139
|
this.tokenManager = tokenManager;
|
|
1752
2140
|
this.pluginId = pluginId;
|
|
1753
2141
|
this.disableDefaultAuthPolicy = disableDefaultAuthPolicy;
|
|
1754
|
-
this.
|
|
2142
|
+
this.pluginKeySource = pluginKeySource;
|
|
1755
2143
|
}
|
|
1756
|
-
|
|
1757
|
-
async authenticate(token) {
|
|
2144
|
+
async authenticate(token, options) {
|
|
1758
2145
|
const pluginResult = await this.pluginTokenHandler.verifyToken(token);
|
|
1759
2146
|
if (pluginResult) {
|
|
1760
2147
|
if (pluginResult.limitedUserToken) {
|
|
@@ -1776,6 +2163,9 @@ class DefaultAuthService {
|
|
|
1776
2163
|
}
|
|
1777
2164
|
const userResult = await this.userTokenHandler.verifyToken(token);
|
|
1778
2165
|
if (userResult) {
|
|
2166
|
+
if (!options?.allowLimitedAccess && this.userTokenHandler.isLimitedUserToken(token)) {
|
|
2167
|
+
throw new errors.AuthenticationError("Illegal limited user token");
|
|
2168
|
+
}
|
|
1779
2169
|
return createCredentialsWithUserPrincipal(
|
|
1780
2170
|
userResult.userEntityRef,
|
|
1781
2171
|
token,
|
|
@@ -1788,920 +2178,972 @@ class DefaultAuthService {
|
|
|
1788
2178
|
externalResult.subject,
|
|
1789
2179
|
void 0,
|
|
1790
2180
|
externalResult.accessRestrictions
|
|
1791
|
-
);
|
|
1792
|
-
}
|
|
1793
|
-
throw new errors.AuthenticationError("Illegal token");
|
|
1794
|
-
}
|
|
1795
|
-
isPrincipal(credentials, type) {
|
|
1796
|
-
const principal = credentials.principal;
|
|
1797
|
-
if (type === "unknown") {
|
|
1798
|
-
return true;
|
|
1799
|
-
}
|
|
1800
|
-
if (principal.type !== type) {
|
|
1801
|
-
return false;
|
|
1802
|
-
}
|
|
1803
|
-
return true;
|
|
1804
|
-
}
|
|
1805
|
-
async getNoneCredentials() {
|
|
1806
|
-
return createCredentialsWithNonePrincipal();
|
|
1807
|
-
}
|
|
1808
|
-
async getOwnServiceCredentials() {
|
|
1809
|
-
return createCredentialsWithServicePrincipal(`plugin:${this.pluginId}`);
|
|
1810
|
-
}
|
|
1811
|
-
async getPluginRequestToken(options) {
|
|
1812
|
-
const { targetPluginId } = options;
|
|
1813
|
-
const internalForward = toInternalBackstageCredentials(options.onBehalfOf);
|
|
1814
|
-
const { type } = internalForward.principal;
|
|
1815
|
-
if (type === "none" && this.disableDefaultAuthPolicy) {
|
|
1816
|
-
return { token: "" };
|
|
1817
|
-
}
|
|
1818
|
-
const targetSupportsNewAuth = await this.pluginTokenHandler.isTargetPluginSupported(targetPluginId);
|
|
1819
|
-
switch (type) {
|
|
1820
|
-
case "service":
|
|
1821
|
-
if (targetSupportsNewAuth) {
|
|
1822
|
-
return this.pluginTokenHandler.issueToken({
|
|
1823
|
-
pluginId: this.pluginId,
|
|
1824
|
-
targetPluginId
|
|
1825
|
-
});
|
|
1826
|
-
}
|
|
1827
|
-
return this.tokenManager.getToken().catch((error) => {
|
|
1828
|
-
throw new errors.ForwardedError(
|
|
1829
|
-
`Unable to generate legacy token for communication with the '${targetPluginId}' plugin. You will typically encounter this error when attempting to call a plugin that does not exist, or is deployed with an old version of Backstage`,
|
|
1830
|
-
error
|
|
1831
|
-
);
|
|
1832
|
-
});
|
|
1833
|
-
case "user": {
|
|
1834
|
-
const { token } = internalForward;
|
|
1835
|
-
if (!token) {
|
|
1836
|
-
throw new Error("User credentials is unexpectedly missing token");
|
|
1837
|
-
}
|
|
1838
|
-
if (targetSupportsNewAuth) {
|
|
1839
|
-
const onBehalfOf = await this.userTokenHandler.createLimitedUserToken(
|
|
1840
|
-
token
|
|
1841
|
-
);
|
|
1842
|
-
return this.pluginTokenHandler.issueToken({
|
|
1843
|
-
pluginId: this.pluginId,
|
|
1844
|
-
targetPluginId,
|
|
1845
|
-
onBehalfOf
|
|
1846
|
-
});
|
|
1847
|
-
}
|
|
1848
|
-
if (this.userTokenHandler.isLimitedUserToken(token)) {
|
|
1849
|
-
throw new errors.AuthenticationError(
|
|
1850
|
-
`Unable to call '${targetPluginId}' plugin on behalf of user, because the target plugin does not support on-behalf-of tokens or the plugin doesn't exist`
|
|
1851
|
-
);
|
|
1852
|
-
}
|
|
1853
|
-
return { token };
|
|
1854
|
-
}
|
|
1855
|
-
default:
|
|
1856
|
-
throw new errors.AuthenticationError(
|
|
1857
|
-
`Refused to issue service token for credential type '${type}'`
|
|
1858
|
-
);
|
|
1859
|
-
}
|
|
1860
|
-
}
|
|
1861
|
-
async getLimitedUserToken(credentials) {
|
|
1862
|
-
const { token: backstageToken } = toInternalBackstageCredentials(credentials);
|
|
1863
|
-
if (!backstageToken) {
|
|
1864
|
-
throw new errors.AuthenticationError(
|
|
1865
|
-
"User credentials is unexpectedly missing token"
|
|
1866
|
-
);
|
|
1867
|
-
}
|
|
1868
|
-
return this.userTokenHandler.createLimitedUserToken(backstageToken);
|
|
1869
|
-
}
|
|
1870
|
-
async listPublicServiceKeys() {
|
|
1871
|
-
const { keys } = await this.publicKeyStore.listKeys();
|
|
1872
|
-
return { keys: keys.map(({ key }) => key) };
|
|
1873
|
-
}
|
|
1874
|
-
#getJwtExpiration(token) {
|
|
1875
|
-
const { exp } = jose.decodeJwt(token);
|
|
1876
|
-
if (!exp) {
|
|
1877
|
-
throw new errors.AuthenticationError("User token is missing expiration");
|
|
1878
|
-
}
|
|
1879
|
-
return new Date(exp * 1e3);
|
|
1880
|
-
}
|
|
1881
|
-
}
|
|
1882
|
-
|
|
1883
|
-
const CLOCK_MARGIN_S = 10;
|
|
1884
|
-
class JwksClient {
|
|
1885
|
-
constructor(getEndpoint) {
|
|
1886
|
-
this.getEndpoint = getEndpoint;
|
|
1887
|
-
}
|
|
1888
|
-
#keyStore;
|
|
1889
|
-
#keyStoreUpdated = 0;
|
|
1890
|
-
get getKey() {
|
|
1891
|
-
if (!this.#keyStore) {
|
|
1892
|
-
throw new errors.AuthenticationError(
|
|
1893
|
-
"refreshKeyStore must be called before jwksClient.getKey"
|
|
1894
|
-
);
|
|
1895
|
-
}
|
|
1896
|
-
return this.#keyStore;
|
|
1897
|
-
}
|
|
1898
|
-
/**
|
|
1899
|
-
* If the last keystore refresh is stale, update the keystore URL to the latest
|
|
1900
|
-
*/
|
|
1901
|
-
async refreshKeyStore(rawJwtToken) {
|
|
1902
|
-
const payload = await jose.decodeJwt(rawJwtToken);
|
|
1903
|
-
const header = await jose.decodeProtectedHeader(rawJwtToken);
|
|
1904
|
-
let keyStoreHasKey;
|
|
1905
|
-
try {
|
|
1906
|
-
if (this.#keyStore) {
|
|
1907
|
-
const [_, rawPayload, rawSignature] = rawJwtToken.split(".");
|
|
1908
|
-
keyStoreHasKey = await this.#keyStore(header, {
|
|
1909
|
-
payload: rawPayload,
|
|
1910
|
-
signature: rawSignature
|
|
1911
|
-
});
|
|
1912
|
-
}
|
|
1913
|
-
} catch (error) {
|
|
1914
|
-
keyStoreHasKey = false;
|
|
1915
|
-
}
|
|
1916
|
-
const issuedAfterLastRefresh = payload?.iat && payload.iat > this.#keyStoreUpdated - CLOCK_MARGIN_S;
|
|
1917
|
-
if (!this.#keyStore || !keyStoreHasKey && issuedAfterLastRefresh) {
|
|
1918
|
-
const endpoint = await this.getEndpoint();
|
|
1919
|
-
this.#keyStore = jose.createRemoteJWKSet(endpoint);
|
|
1920
|
-
this.#keyStoreUpdated = Date.now() / 1e3;
|
|
1921
|
-
}
|
|
1922
|
-
}
|
|
1923
|
-
}
|
|
1924
|
-
|
|
1925
|
-
const KEY_EXPIRATION_MARGIN_FACTOR = 3;
|
|
1926
|
-
const SECONDS_IN_MS = 1e3;
|
|
1927
|
-
const ALLOWED_PLUGIN_ID_PATTERN = /^[a-z0-9_-]+$/i;
|
|
1928
|
-
class PluginTokenHandler {
|
|
1929
|
-
constructor(logger, ownPluginId, publicKeyStore, keyDurationSeconds, algorithm, discovery) {
|
|
1930
|
-
this.logger = logger;
|
|
1931
|
-
this.ownPluginId = ownPluginId;
|
|
1932
|
-
this.publicKeyStore = publicKeyStore;
|
|
1933
|
-
this.keyDurationSeconds = keyDurationSeconds;
|
|
1934
|
-
this.algorithm = algorithm;
|
|
1935
|
-
this.discovery = discovery;
|
|
1936
|
-
}
|
|
1937
|
-
privateKeyPromise;
|
|
1938
|
-
keyExpiry;
|
|
1939
|
-
jwksMap = /* @__PURE__ */ new Map();
|
|
1940
|
-
// Tracking state for isTargetPluginSupported
|
|
1941
|
-
supportedTargetPlugins = /* @__PURE__ */ new Set();
|
|
1942
|
-
targetPluginInflightChecks = /* @__PURE__ */ new Map();
|
|
1943
|
-
static create(options) {
|
|
1944
|
-
return new PluginTokenHandler(
|
|
1945
|
-
options.logger,
|
|
1946
|
-
options.ownPluginId,
|
|
1947
|
-
options.publicKeyStore,
|
|
1948
|
-
Math.round(types.durationToMilliseconds(options.keyDuration) / 1e3),
|
|
1949
|
-
options.algorithm ?? "ES256",
|
|
1950
|
-
options.discovery
|
|
1951
|
-
);
|
|
1952
|
-
}
|
|
1953
|
-
async verifyToken(token) {
|
|
1954
|
-
try {
|
|
1955
|
-
const { typ } = jose.decodeProtectedHeader(token);
|
|
1956
|
-
if (typ !== pluginAuthNode.tokenTypes.plugin.typParam) {
|
|
1957
|
-
return void 0;
|
|
1958
|
-
}
|
|
1959
|
-
} catch {
|
|
1960
|
-
return void 0;
|
|
1961
|
-
}
|
|
1962
|
-
const pluginId = String(jose.decodeJwt(token).sub);
|
|
1963
|
-
if (!pluginId) {
|
|
1964
|
-
throw new errors.AuthenticationError("Invalid plugin token: missing subject");
|
|
1965
|
-
}
|
|
1966
|
-
if (!ALLOWED_PLUGIN_ID_PATTERN.test(pluginId)) {
|
|
1967
|
-
throw new errors.AuthenticationError(
|
|
1968
|
-
"Invalid plugin token: forbidden subject format"
|
|
1969
|
-
);
|
|
1970
|
-
}
|
|
1971
|
-
const jwksClient = await this.getJwksClient(pluginId);
|
|
1972
|
-
await jwksClient.refreshKeyStore(token);
|
|
1973
|
-
const { payload } = await jose.jwtVerify(
|
|
1974
|
-
token,
|
|
1975
|
-
jwksClient.getKey,
|
|
1976
|
-
{
|
|
1977
|
-
typ: pluginAuthNode.tokenTypes.plugin.typParam,
|
|
1978
|
-
audience: this.ownPluginId,
|
|
1979
|
-
requiredClaims: ["iat", "exp", "sub", "aud"]
|
|
1980
|
-
}
|
|
1981
|
-
).catch((e) => {
|
|
1982
|
-
throw new errors.AuthenticationError("Invalid plugin token", e);
|
|
1983
|
-
});
|
|
1984
|
-
return { subject: `plugin:${payload.sub}`, limitedUserToken: payload.obo };
|
|
1985
|
-
}
|
|
1986
|
-
async issueToken(options) {
|
|
1987
|
-
const { pluginId, targetPluginId, onBehalfOf } = options;
|
|
1988
|
-
const key = await this.getKey();
|
|
1989
|
-
const sub = pluginId;
|
|
1990
|
-
const aud = targetPluginId;
|
|
1991
|
-
const iat = Math.floor(Date.now() / SECONDS_IN_MS);
|
|
1992
|
-
const ourExp = iat + this.keyDurationSeconds;
|
|
1993
|
-
const exp = onBehalfOf ? Math.min(
|
|
1994
|
-
ourExp,
|
|
1995
|
-
Math.floor(onBehalfOf.expiresAt.getTime() / SECONDS_IN_MS)
|
|
1996
|
-
) : ourExp;
|
|
1997
|
-
const claims = { sub, aud, iat, exp, obo: onBehalfOf?.token };
|
|
1998
|
-
const token = await new jose.SignJWT(claims).setProtectedHeader({
|
|
1999
|
-
typ: pluginAuthNode.tokenTypes.plugin.typParam,
|
|
2000
|
-
alg: this.algorithm,
|
|
2001
|
-
kid: key.kid
|
|
2002
|
-
}).setAudience(aud).setSubject(sub).setIssuedAt(iat).setExpirationTime(exp).sign(await jose.importJWK(key));
|
|
2003
|
-
return { token };
|
|
2181
|
+
);
|
|
2182
|
+
}
|
|
2183
|
+
throw new errors.AuthenticationError("Illegal token");
|
|
2004
2184
|
}
|
|
2005
|
-
|
|
2006
|
-
|
|
2185
|
+
isPrincipal(credentials, type) {
|
|
2186
|
+
const principal = credentials.principal;
|
|
2187
|
+
if (type === "unknown") {
|
|
2007
2188
|
return true;
|
|
2008
2189
|
}
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
return inFlight;
|
|
2190
|
+
if (principal.type !== type) {
|
|
2191
|
+
return false;
|
|
2012
2192
|
}
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2193
|
+
return true;
|
|
2194
|
+
}
|
|
2195
|
+
async getNoneCredentials() {
|
|
2196
|
+
return createCredentialsWithNonePrincipal();
|
|
2197
|
+
}
|
|
2198
|
+
async getOwnServiceCredentials() {
|
|
2199
|
+
return createCredentialsWithServicePrincipal(`plugin:${this.pluginId}`);
|
|
2200
|
+
}
|
|
2201
|
+
async getPluginRequestToken(options) {
|
|
2202
|
+
const { targetPluginId } = options;
|
|
2203
|
+
const internalForward = toInternalBackstageCredentials(options.onBehalfOf);
|
|
2204
|
+
const { type } = internalForward.principal;
|
|
2205
|
+
if (type === "none" && this.disableDefaultAuthPolicy) {
|
|
2206
|
+
return { token: "" };
|
|
2207
|
+
}
|
|
2208
|
+
const targetSupportsNewAuth = await this.pluginTokenHandler.isTargetPluginSupported(targetPluginId);
|
|
2209
|
+
switch (type) {
|
|
2210
|
+
case "service":
|
|
2211
|
+
if (targetSupportsNewAuth) {
|
|
2212
|
+
return this.pluginTokenHandler.issueToken({
|
|
2213
|
+
pluginId: this.pluginId,
|
|
2017
2214
|
targetPluginId
|
|
2018
|
-
)
|
|
2019
|
-
);
|
|
2020
|
-
if (res.status === 404) {
|
|
2021
|
-
return false;
|
|
2215
|
+
});
|
|
2022
2216
|
}
|
|
2023
|
-
|
|
2024
|
-
throw new
|
|
2217
|
+
return this.tokenManager.getToken().catch((error) => {
|
|
2218
|
+
throw new errors.ForwardedError(
|
|
2219
|
+
`Unable to generate legacy token for communication with the '${targetPluginId}' plugin. You will typically encounter this error when attempting to call a plugin that does not exist, or is deployed with an old version of Backstage`,
|
|
2220
|
+
error
|
|
2221
|
+
);
|
|
2222
|
+
});
|
|
2223
|
+
case "user": {
|
|
2224
|
+
const { token } = internalForward;
|
|
2225
|
+
if (!token) {
|
|
2226
|
+
throw new Error("User credentials is unexpectedly missing token");
|
|
2025
2227
|
}
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2228
|
+
if (targetSupportsNewAuth) {
|
|
2229
|
+
const onBehalfOf = await this.userTokenHandler.createLimitedUserToken(
|
|
2230
|
+
token
|
|
2231
|
+
);
|
|
2232
|
+
return this.pluginTokenHandler.issueToken({
|
|
2233
|
+
pluginId: this.pluginId,
|
|
2234
|
+
targetPluginId,
|
|
2235
|
+
onBehalfOf
|
|
2236
|
+
});
|
|
2029
2237
|
}
|
|
2030
|
-
this.
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
this.targetPluginInflightChecks.delete(targetPluginId);
|
|
2238
|
+
if (this.userTokenHandler.isLimitedUserToken(token)) {
|
|
2239
|
+
throw new errors.AuthenticationError(
|
|
2240
|
+
`Unable to call '${targetPluginId}' plugin on behalf of user, because the target plugin does not support on-behalf-of tokens or the plugin doesn't exist`
|
|
2241
|
+
);
|
|
2242
|
+
}
|
|
2243
|
+
return { token };
|
|
2037
2244
|
}
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
}
|
|
2043
|
-
async getJwksClient(pluginId) {
|
|
2044
|
-
const client = this.jwksMap.get(pluginId);
|
|
2045
|
-
if (client) {
|
|
2046
|
-
return client;
|
|
2245
|
+
default:
|
|
2246
|
+
throw new errors.AuthenticationError(
|
|
2247
|
+
`Refused to issue service token for credential type '${type}'`
|
|
2248
|
+
);
|
|
2047
2249
|
}
|
|
2048
|
-
|
|
2250
|
+
}
|
|
2251
|
+
async getLimitedUserToken(credentials) {
|
|
2252
|
+
const { token: backstageToken } = toInternalBackstageCredentials(credentials);
|
|
2253
|
+
if (!backstageToken) {
|
|
2049
2254
|
throw new errors.AuthenticationError(
|
|
2050
|
-
|
|
2255
|
+
"User credentials is unexpectedly missing token"
|
|
2051
2256
|
);
|
|
2052
2257
|
}
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2258
|
+
return this.userTokenHandler.createLimitedUserToken(backstageToken);
|
|
2259
|
+
}
|
|
2260
|
+
async listPublicServiceKeys() {
|
|
2261
|
+
const { keys } = await this.pluginKeySource.listKeys();
|
|
2262
|
+
return { keys: keys.map(({ key }) => key) };
|
|
2263
|
+
}
|
|
2264
|
+
#getJwtExpiration(token) {
|
|
2265
|
+
const { exp } = jose.decodeJwt(token);
|
|
2266
|
+
if (!exp) {
|
|
2267
|
+
throw new errors.AuthenticationError("User token is missing expiration");
|
|
2268
|
+
}
|
|
2269
|
+
return new Date(exp * 1e3);
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
function readAccessRestrictionsFromConfig(externalAccessEntryConfig) {
|
|
2274
|
+
const configs = externalAccessEntryConfig.getOptionalConfigArray("accessRestrictions") ?? [];
|
|
2275
|
+
const result = /* @__PURE__ */ new Map();
|
|
2276
|
+
for (const config of configs) {
|
|
2277
|
+
const validKeys = ["plugin", "permission", "permissionAttribute"];
|
|
2278
|
+
for (const key of config.keys()) {
|
|
2279
|
+
if (!validKeys.includes(key)) {
|
|
2280
|
+
const valid = validKeys.map((k) => `'${k}'`).join(", ");
|
|
2281
|
+
throw new Error(
|
|
2282
|
+
`Invalid key '${key}' in 'accessRestrictions' config, expected one of ${valid}`
|
|
2283
|
+
);
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
const pluginId = config.getString("plugin");
|
|
2287
|
+
const permissionNames = readPermissionNames(config);
|
|
2288
|
+
const permissionAttributes = readPermissionAttributes(config);
|
|
2289
|
+
if (result.has(pluginId)) {
|
|
2290
|
+
throw new Error(
|
|
2291
|
+
`Attempted to declare 'accessRestrictions' twice for plugin '${pluginId}', which is not permitted`
|
|
2058
2292
|
);
|
|
2293
|
+
}
|
|
2294
|
+
result.set(pluginId, {
|
|
2295
|
+
...permissionNames ? { permissionNames } : {},
|
|
2296
|
+
...permissionAttributes ? { permissionAttributes } : {}
|
|
2059
2297
|
});
|
|
2060
|
-
this.jwksMap.set(pluginId, newClient);
|
|
2061
|
-
return newClient;
|
|
2062
2298
|
}
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2299
|
+
return result.size ? result : void 0;
|
|
2300
|
+
}
|
|
2301
|
+
function readStringOrStringArrayFromConfig(root, key, validValues) {
|
|
2302
|
+
if (!root.has(key)) {
|
|
2303
|
+
return void 0;
|
|
2304
|
+
}
|
|
2305
|
+
const rawValues = Array.isArray(root.get(key)) ? root.getStringArray(key) : [root.getString(key)];
|
|
2306
|
+
const values = [
|
|
2307
|
+
...new Set(
|
|
2308
|
+
rawValues.map((v) => v.split(/[ ,]/)).flat().filter(Boolean)
|
|
2309
|
+
)
|
|
2310
|
+
];
|
|
2311
|
+
if (!values.length) {
|
|
2312
|
+
return void 0;
|
|
2313
|
+
}
|
|
2314
|
+
if (validValues?.length) {
|
|
2315
|
+
for (const value of values) {
|
|
2316
|
+
if (!validValues.includes(value)) {
|
|
2317
|
+
const valid = validValues.map((k) => `'${k}'`).join(", ");
|
|
2318
|
+
throw new Error(
|
|
2319
|
+
`Invalid value '${value}' at '${key}' in 'permissionAttributes' config, valid values are ${valid}`
|
|
2320
|
+
);
|
|
2067
2321
|
}
|
|
2068
|
-
this.logger.info(`Signing key has expired, generating new key`);
|
|
2069
|
-
delete this.privateKeyPromise;
|
|
2070
2322
|
}
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
} catch (error) {
|
|
2095
|
-
this.logger.error(`Failed to generate new signing key, ${error}`);
|
|
2096
|
-
delete this.keyExpiry;
|
|
2097
|
-
delete this.privateKeyPromise;
|
|
2323
|
+
}
|
|
2324
|
+
return values;
|
|
2325
|
+
}
|
|
2326
|
+
function readPermissionNames(externalAccessEntryConfig) {
|
|
2327
|
+
return readStringOrStringArrayFromConfig(
|
|
2328
|
+
externalAccessEntryConfig,
|
|
2329
|
+
"permission"
|
|
2330
|
+
);
|
|
2331
|
+
}
|
|
2332
|
+
function readPermissionAttributes(externalAccessEntryConfig) {
|
|
2333
|
+
const config = externalAccessEntryConfig.getOptionalConfig(
|
|
2334
|
+
"permissionAttribute"
|
|
2335
|
+
);
|
|
2336
|
+
if (!config) {
|
|
2337
|
+
return void 0;
|
|
2338
|
+
}
|
|
2339
|
+
const validKeys = ["action"];
|
|
2340
|
+
for (const key of config.keys()) {
|
|
2341
|
+
if (!validKeys.includes(key)) {
|
|
2342
|
+
const valid = validKeys.map((k) => `'${k}'`).join(", ");
|
|
2343
|
+
throw new Error(
|
|
2344
|
+
`Invalid key '${key}' in 'permissionAttribute' config, expected ${valid}`
|
|
2345
|
+
);
|
|
2098
2346
|
}
|
|
2099
|
-
return promise;
|
|
2100
2347
|
}
|
|
2348
|
+
const action = readStringOrStringArrayFromConfig(config, "action", [
|
|
2349
|
+
"create",
|
|
2350
|
+
"read",
|
|
2351
|
+
"update",
|
|
2352
|
+
"delete"
|
|
2353
|
+
]);
|
|
2354
|
+
const result = {
|
|
2355
|
+
...action ? { action } : {}
|
|
2356
|
+
};
|
|
2357
|
+
return Object.keys(result).length ? result : void 0;
|
|
2101
2358
|
}
|
|
2102
2359
|
|
|
2103
|
-
class
|
|
2104
|
-
|
|
2105
|
-
|
|
2360
|
+
class LegacyTokenHandler {
|
|
2361
|
+
#entries = new Array();
|
|
2362
|
+
add(config) {
|
|
2363
|
+
const allAccessRestrictions = readAccessRestrictionsFromConfig(config);
|
|
2364
|
+
this.#doAdd(
|
|
2365
|
+
config.getString("options.secret"),
|
|
2366
|
+
config.getString("options.subject"),
|
|
2367
|
+
allAccessRestrictions
|
|
2368
|
+
);
|
|
2106
2369
|
}
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
return new URL(`${url}/.well-known/jwks.json`);
|
|
2111
|
-
});
|
|
2112
|
-
return new UserTokenHandler(jwksClient);
|
|
2370
|
+
// used only for the old backend.auth.keys array
|
|
2371
|
+
addOld(config) {
|
|
2372
|
+
this.#doAdd(config.getString("secret"), "external:backstage-plugin");
|
|
2113
2373
|
}
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2374
|
+
#doAdd(secret, subject, allAccessRestrictions) {
|
|
2375
|
+
if (!secret.match(/^\S+$/)) {
|
|
2376
|
+
throw new Error("Illegal secret, must be a valid base64 string");
|
|
2377
|
+
} else if (!subject.match(/^\S+$/)) {
|
|
2378
|
+
throw new Error("Illegal subject, must be a set of non-space characters");
|
|
2118
2379
|
}
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
).catch((e) => {
|
|
2125
|
-
throw new errors.AuthenticationError("Invalid token", e);
|
|
2126
|
-
});
|
|
2127
|
-
const userEntityRef = payload.sub;
|
|
2128
|
-
if (!userEntityRef) {
|
|
2129
|
-
throw new errors.AuthenticationError("No user sub found in token");
|
|
2380
|
+
let key;
|
|
2381
|
+
try {
|
|
2382
|
+
key = jose.base64url.decode(secret);
|
|
2383
|
+
} catch {
|
|
2384
|
+
throw new Error("Illegal secret, must be a valid base64 string");
|
|
2130
2385
|
}
|
|
2131
|
-
|
|
2386
|
+
if (this.#entries.some((e) => e.key === key)) {
|
|
2387
|
+
throw new Error(
|
|
2388
|
+
"Legacy externalAccess token was declared more than once"
|
|
2389
|
+
);
|
|
2390
|
+
}
|
|
2391
|
+
this.#entries.push({
|
|
2392
|
+
key,
|
|
2393
|
+
result: {
|
|
2394
|
+
subject,
|
|
2395
|
+
allAccessRestrictions
|
|
2396
|
+
}
|
|
2397
|
+
});
|
|
2132
2398
|
}
|
|
2133
|
-
|
|
2399
|
+
async verifyToken(token) {
|
|
2134
2400
|
try {
|
|
2135
|
-
const {
|
|
2136
|
-
if (
|
|
2137
|
-
return
|
|
2138
|
-
requiredClaims: ["iat", "exp", "sub"],
|
|
2139
|
-
typ: pluginAuthNode.tokenTypes.user.typParam
|
|
2140
|
-
};
|
|
2401
|
+
const { alg } = jose.decodeProtectedHeader(token);
|
|
2402
|
+
if (alg !== "HS256") {
|
|
2403
|
+
return void 0;
|
|
2141
2404
|
}
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
typ: pluginAuthNode.tokenTypes.limitedUser.typParam
|
|
2146
|
-
};
|
|
2405
|
+
const { sub, aud } = jose.decodeJwt(token);
|
|
2406
|
+
if (sub !== "backstage-server" || aud) {
|
|
2407
|
+
return void 0;
|
|
2147
2408
|
}
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2409
|
+
} catch (e) {
|
|
2410
|
+
return void 0;
|
|
2411
|
+
}
|
|
2412
|
+
for (const { key, result } of this.#entries) {
|
|
2413
|
+
try {
|
|
2414
|
+
await jose.jwtVerify(token, key);
|
|
2415
|
+
return result;
|
|
2416
|
+
} catch (e) {
|
|
2417
|
+
if (e.code !== "ERR_JWS_SIGNATURE_VERIFICATION_FAILED") {
|
|
2418
|
+
throw e;
|
|
2419
|
+
}
|
|
2153
2420
|
}
|
|
2154
|
-
} catch {
|
|
2155
2421
|
}
|
|
2156
2422
|
return void 0;
|
|
2157
2423
|
}
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
);
|
|
2166
|
-
const
|
|
2167
|
-
if (!
|
|
2168
|
-
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
const MIN_TOKEN_LENGTH = 8;
|
|
2427
|
+
class StaticTokenHandler {
|
|
2428
|
+
#entries = /* @__PURE__ */ new Map();
|
|
2429
|
+
add(config) {
|
|
2430
|
+
const token = config.getString("options.token");
|
|
2431
|
+
const subject = config.getString("options.subject");
|
|
2432
|
+
const allAccessRestrictions = readAccessRestrictionsFromConfig(config);
|
|
2433
|
+
if (!token.match(/^\S+$/)) {
|
|
2434
|
+
throw new Error("Illegal token, must be a set of non-space characters");
|
|
2435
|
+
} else if (token.length < MIN_TOKEN_LENGTH) {
|
|
2436
|
+
throw new Error(
|
|
2437
|
+
`Illegal token, must be at least ${MIN_TOKEN_LENGTH} characters length`
|
|
2438
|
+
);
|
|
2439
|
+
} else if (!subject.match(/^\S+$/)) {
|
|
2440
|
+
throw new Error("Illegal subject, must be a set of non-space characters");
|
|
2441
|
+
} else if (this.#entries.has(token)) {
|
|
2442
|
+
throw new Error(
|
|
2443
|
+
"Static externalAccess token was declared more than once"
|
|
2444
|
+
);
|
|
2169
2445
|
}
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2446
|
+
this.#entries.set(token, { subject, allAccessRestrictions });
|
|
2447
|
+
}
|
|
2448
|
+
async verifyToken(token) {
|
|
2449
|
+
return this.#entries.get(token);
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
class JWKSHandler {
|
|
2454
|
+
#entries = [];
|
|
2455
|
+
add(config) {
|
|
2456
|
+
if (!config.getString("options.url").match(/^\S+$/)) {
|
|
2457
|
+
throw new Error(
|
|
2458
|
+
"Illegal JWKS URL, must be a set of non-space characters"
|
|
2173
2459
|
);
|
|
2174
2460
|
}
|
|
2175
|
-
const
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2461
|
+
const algorithms = readStringOrStringArrayFromConfig(
|
|
2462
|
+
config,
|
|
2463
|
+
"options.algorithm"
|
|
2464
|
+
);
|
|
2465
|
+
const issuers = readStringOrStringArrayFromConfig(config, "options.issuer");
|
|
2466
|
+
const audiences = readStringOrStringArrayFromConfig(
|
|
2467
|
+
config,
|
|
2468
|
+
"options.audience"
|
|
2469
|
+
);
|
|
2470
|
+
const subjectPrefix = config.getOptionalString("options.subjectPrefix");
|
|
2471
|
+
const url = new URL(config.getString("options.url"));
|
|
2472
|
+
const jwks = jose.createRemoteJWKSet(url);
|
|
2473
|
+
const allAccessRestrictions = readAccessRestrictionsFromConfig(config);
|
|
2474
|
+
this.#entries.push({
|
|
2475
|
+
algorithms,
|
|
2476
|
+
audiences,
|
|
2477
|
+
issuers,
|
|
2478
|
+
jwks,
|
|
2479
|
+
subjectPrefix,
|
|
2480
|
+
url,
|
|
2481
|
+
allAccessRestrictions
|
|
2482
|
+
});
|
|
2194
2483
|
}
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2484
|
+
async verifyToken(token) {
|
|
2485
|
+
for (const entry of this.#entries) {
|
|
2486
|
+
try {
|
|
2487
|
+
const {
|
|
2488
|
+
payload: { sub }
|
|
2489
|
+
} = await jose.jwtVerify(token, entry.jwks, {
|
|
2490
|
+
algorithms: entry.algorithms,
|
|
2491
|
+
issuer: entry.issuers,
|
|
2492
|
+
audience: entry.audiences
|
|
2493
|
+
});
|
|
2494
|
+
if (sub) {
|
|
2495
|
+
const prefix = entry.subjectPrefix ? `external:${entry.subjectPrefix}:` : "external:";
|
|
2496
|
+
return {
|
|
2497
|
+
subject: `${prefix}${sub}`,
|
|
2498
|
+
allAccessRestrictions: entry.allAccessRestrictions
|
|
2499
|
+
};
|
|
2500
|
+
}
|
|
2501
|
+
} catch {
|
|
2502
|
+
continue;
|
|
2503
|
+
}
|
|
2201
2504
|
}
|
|
2505
|
+
return void 0;
|
|
2202
2506
|
}
|
|
2203
2507
|
}
|
|
2204
2508
|
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2509
|
+
const NEW_CONFIG_KEY = "backend.auth.externalAccess";
|
|
2510
|
+
const OLD_CONFIG_KEY = "backend.auth.keys";
|
|
2511
|
+
let loggedDeprecationWarning = false;
|
|
2512
|
+
class ExternalTokenHandler {
|
|
2513
|
+
constructor(ownPluginId, handlers) {
|
|
2514
|
+
this.ownPluginId = ownPluginId;
|
|
2515
|
+
this.handlers = handlers;
|
|
2516
|
+
}
|
|
2517
|
+
static create(options) {
|
|
2518
|
+
const { ownPluginId, config, logger } = options;
|
|
2519
|
+
const staticHandler = new StaticTokenHandler();
|
|
2520
|
+
const legacyHandler = new LegacyTokenHandler();
|
|
2521
|
+
const jwksHandler = new JWKSHandler();
|
|
2522
|
+
const handlers = {
|
|
2523
|
+
static: staticHandler,
|
|
2524
|
+
legacy: legacyHandler,
|
|
2525
|
+
jwks: jwksHandler
|
|
2526
|
+
};
|
|
2527
|
+
const handlerConfigs = config.getOptionalConfigArray(NEW_CONFIG_KEY) ?? [];
|
|
2528
|
+
for (const handlerConfig of handlerConfigs) {
|
|
2529
|
+
const type = handlerConfig.getString("type");
|
|
2530
|
+
const handler = handlers[type];
|
|
2531
|
+
if (!handler) {
|
|
2532
|
+
const valid = Object.keys(handlers).map((k) => `'${k}'`).join(", ");
|
|
2213
2533
|
throw new Error(
|
|
2214
|
-
`
|
|
2534
|
+
`Unknown type '${type}' in ${NEW_CONFIG_KEY}, expected one of ${valid}`
|
|
2215
2535
|
);
|
|
2216
2536
|
}
|
|
2537
|
+
handler.add(handlerConfig);
|
|
2217
2538
|
}
|
|
2218
|
-
const
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
`Attempted to declare 'accessRestrictions' twice for plugin '${pluginId}', which is not permitted`
|
|
2539
|
+
const legacyConfigs = config.getOptionalConfigArray(OLD_CONFIG_KEY) ?? [];
|
|
2540
|
+
if (legacyConfigs.length && !loggedDeprecationWarning) {
|
|
2541
|
+
loggedDeprecationWarning = true;
|
|
2542
|
+
logger.warn(
|
|
2543
|
+
`DEPRECATION WARNING: The ${OLD_CONFIG_KEY} config has been replaced by ${NEW_CONFIG_KEY}, see https://backstage.io/docs/auth/service-to-service-auth`
|
|
2224
2544
|
);
|
|
2225
2545
|
}
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2546
|
+
for (const handlerConfig of legacyConfigs) {
|
|
2547
|
+
legacyHandler.addOld(handlerConfig);
|
|
2548
|
+
}
|
|
2549
|
+
return new ExternalTokenHandler(ownPluginId, Object.values(handlers));
|
|
2550
|
+
}
|
|
2551
|
+
async verifyToken(token) {
|
|
2552
|
+
for (const handler of this.handlers) {
|
|
2553
|
+
const result = await handler.verifyToken(token);
|
|
2554
|
+
if (result) {
|
|
2555
|
+
const { allAccessRestrictions, ...rest } = result;
|
|
2556
|
+
if (allAccessRestrictions) {
|
|
2557
|
+
const accessRestrictions = allAccessRestrictions.get(
|
|
2558
|
+
this.ownPluginId
|
|
2559
|
+
);
|
|
2560
|
+
if (!accessRestrictions) {
|
|
2561
|
+
const valid = [...allAccessRestrictions.keys()].map((k) => `'${k}'`).join(", ");
|
|
2562
|
+
throw new errors.NotAllowedError(
|
|
2563
|
+
`This token's access is restricted to plugin(s) ${valid}`
|
|
2564
|
+
);
|
|
2565
|
+
}
|
|
2566
|
+
return {
|
|
2567
|
+
...rest,
|
|
2568
|
+
accessRestrictions
|
|
2569
|
+
};
|
|
2570
|
+
}
|
|
2571
|
+
return rest;
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
return void 0;
|
|
2230
2575
|
}
|
|
2231
|
-
return result.size ? result : void 0;
|
|
2232
2576
|
}
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2577
|
+
|
|
2578
|
+
const CLOCK_MARGIN_S = 10;
|
|
2579
|
+
class JwksClient {
|
|
2580
|
+
constructor(getEndpoint) {
|
|
2581
|
+
this.getEndpoint = getEndpoint;
|
|
2236
2582
|
}
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2583
|
+
#keyStore;
|
|
2584
|
+
#keyStoreUpdated = 0;
|
|
2585
|
+
get getKey() {
|
|
2586
|
+
if (!this.#keyStore) {
|
|
2587
|
+
throw new errors.AuthenticationError(
|
|
2588
|
+
"refreshKeyStore must be called before jwksClient.getKey"
|
|
2589
|
+
);
|
|
2590
|
+
}
|
|
2591
|
+
return this.#keyStore;
|
|
2245
2592
|
}
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2593
|
+
/**
|
|
2594
|
+
* If the last keystore refresh is stale, update the keystore URL to the latest
|
|
2595
|
+
*/
|
|
2596
|
+
async refreshKeyStore(rawJwtToken) {
|
|
2597
|
+
const payload = await jose.decodeJwt(rawJwtToken);
|
|
2598
|
+
const header = await jose.decodeProtectedHeader(rawJwtToken);
|
|
2599
|
+
let keyStoreHasKey;
|
|
2600
|
+
try {
|
|
2601
|
+
if (this.#keyStore) {
|
|
2602
|
+
const [_, rawPayload, rawSignature] = rawJwtToken.split(".");
|
|
2603
|
+
keyStoreHasKey = await this.#keyStore(header, {
|
|
2604
|
+
payload: rawPayload,
|
|
2605
|
+
signature: rawSignature
|
|
2606
|
+
});
|
|
2253
2607
|
}
|
|
2608
|
+
} catch (error) {
|
|
2609
|
+
keyStoreHasKey = false;
|
|
2254
2610
|
}
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
externalAccessEntryConfig,
|
|
2261
|
-
"permission"
|
|
2262
|
-
);
|
|
2263
|
-
}
|
|
2264
|
-
function readPermissionAttributes(externalAccessEntryConfig) {
|
|
2265
|
-
const config = externalAccessEntryConfig.getOptionalConfig(
|
|
2266
|
-
"permissionAttribute"
|
|
2267
|
-
);
|
|
2268
|
-
if (!config) {
|
|
2269
|
-
return void 0;
|
|
2270
|
-
}
|
|
2271
|
-
const validKeys = ["action"];
|
|
2272
|
-
for (const key of config.keys()) {
|
|
2273
|
-
if (!validKeys.includes(key)) {
|
|
2274
|
-
const valid = validKeys.map((k) => `'${k}'`).join(", ");
|
|
2275
|
-
throw new Error(
|
|
2276
|
-
`Invalid key '${key}' in 'permissionAttribute' config, expected ${valid}`
|
|
2277
|
-
);
|
|
2611
|
+
const issuedAfterLastRefresh = payload?.iat && payload.iat > this.#keyStoreUpdated - CLOCK_MARGIN_S;
|
|
2612
|
+
if (!this.#keyStore || !keyStoreHasKey && issuedAfterLastRefresh) {
|
|
2613
|
+
const endpoint = await this.getEndpoint();
|
|
2614
|
+
this.#keyStore = jose.createRemoteJWKSet(endpoint);
|
|
2615
|
+
this.#keyStoreUpdated = Date.now() / 1e3;
|
|
2278
2616
|
}
|
|
2279
2617
|
}
|
|
2280
|
-
const action = readStringOrStringArrayFromConfig(config, "action", [
|
|
2281
|
-
"create",
|
|
2282
|
-
"read",
|
|
2283
|
-
"update",
|
|
2284
|
-
"delete"
|
|
2285
|
-
]);
|
|
2286
|
-
const result = {
|
|
2287
|
-
...action ? { action } : {}
|
|
2288
|
-
};
|
|
2289
|
-
return Object.keys(result).length ? result : void 0;
|
|
2290
2618
|
}
|
|
2291
2619
|
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
this
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2620
|
+
const SECONDS_IN_MS$2 = 1e3;
|
|
2621
|
+
const ALLOWED_PLUGIN_ID_PATTERN = /^[a-z0-9_-]+$/i;
|
|
2622
|
+
class PluginTokenHandler {
|
|
2623
|
+
constructor(logger, ownPluginId, keySource, algorithm, keyDurationSeconds, discovery) {
|
|
2624
|
+
this.logger = logger;
|
|
2625
|
+
this.ownPluginId = ownPluginId;
|
|
2626
|
+
this.keySource = keySource;
|
|
2627
|
+
this.algorithm = algorithm;
|
|
2628
|
+
this.keyDurationSeconds = keyDurationSeconds;
|
|
2629
|
+
this.discovery = discovery;
|
|
2301
2630
|
}
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2631
|
+
jwksMap = /* @__PURE__ */ new Map();
|
|
2632
|
+
// Tracking state for isTargetPluginSupported
|
|
2633
|
+
supportedTargetPlugins = /* @__PURE__ */ new Set();
|
|
2634
|
+
targetPluginInflightChecks = /* @__PURE__ */ new Map();
|
|
2635
|
+
static create(options) {
|
|
2636
|
+
return new PluginTokenHandler(
|
|
2637
|
+
options.logger,
|
|
2638
|
+
options.ownPluginId,
|
|
2639
|
+
options.keySource,
|
|
2640
|
+
options.algorithm ?? "ES256",
|
|
2641
|
+
Math.round(types.durationToMilliseconds(options.keyDuration) / 1e3),
|
|
2642
|
+
options.discovery
|
|
2643
|
+
);
|
|
2305
2644
|
}
|
|
2306
|
-
|
|
2307
|
-
if (!secret.match(/^\S+$/)) {
|
|
2308
|
-
throw new Error("Illegal secret, must be a valid base64 string");
|
|
2309
|
-
} else if (!subject.match(/^\S+$/)) {
|
|
2310
|
-
throw new Error("Illegal subject, must be a set of non-space characters");
|
|
2311
|
-
}
|
|
2312
|
-
let key;
|
|
2645
|
+
async verifyToken(token) {
|
|
2313
2646
|
try {
|
|
2314
|
-
|
|
2647
|
+
const { typ } = jose.decodeProtectedHeader(token);
|
|
2648
|
+
if (typ !== pluginAuthNode.tokenTypes.plugin.typParam) {
|
|
2649
|
+
return void 0;
|
|
2650
|
+
}
|
|
2315
2651
|
} catch {
|
|
2316
|
-
|
|
2652
|
+
return void 0;
|
|
2317
2653
|
}
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2654
|
+
const pluginId = String(jose.decodeJwt(token).sub);
|
|
2655
|
+
if (!pluginId) {
|
|
2656
|
+
throw new errors.AuthenticationError("Invalid plugin token: missing subject");
|
|
2657
|
+
}
|
|
2658
|
+
if (!ALLOWED_PLUGIN_ID_PATTERN.test(pluginId)) {
|
|
2659
|
+
throw new errors.AuthenticationError(
|
|
2660
|
+
"Invalid plugin token: forbidden subject format"
|
|
2321
2661
|
);
|
|
2322
2662
|
}
|
|
2323
|
-
this
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2663
|
+
const jwksClient = await this.getJwksClient(pluginId);
|
|
2664
|
+
await jwksClient.refreshKeyStore(token);
|
|
2665
|
+
const { payload } = await jose.jwtVerify(
|
|
2666
|
+
token,
|
|
2667
|
+
jwksClient.getKey,
|
|
2668
|
+
{
|
|
2669
|
+
typ: pluginAuthNode.tokenTypes.plugin.typParam,
|
|
2670
|
+
audience: this.ownPluginId,
|
|
2671
|
+
requiredClaims: ["iat", "exp", "sub", "aud"]
|
|
2328
2672
|
}
|
|
2673
|
+
).catch((e) => {
|
|
2674
|
+
throw new errors.AuthenticationError("Invalid plugin token", e);
|
|
2329
2675
|
});
|
|
2676
|
+
return { subject: `plugin:${payload.sub}`, limitedUserToken: payload.obo };
|
|
2330
2677
|
}
|
|
2331
|
-
async
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2678
|
+
async issueToken(options) {
|
|
2679
|
+
const { pluginId, targetPluginId, onBehalfOf } = options;
|
|
2680
|
+
const key = await this.keySource.getPrivateSigningKey();
|
|
2681
|
+
const sub = pluginId;
|
|
2682
|
+
const aud = targetPluginId;
|
|
2683
|
+
const iat = Math.floor(Date.now() / SECONDS_IN_MS$2);
|
|
2684
|
+
const ourExp = iat + this.keyDurationSeconds;
|
|
2685
|
+
const exp = onBehalfOf ? Math.min(
|
|
2686
|
+
ourExp,
|
|
2687
|
+
Math.floor(onBehalfOf.expiresAt.getTime() / SECONDS_IN_MS$2)
|
|
2688
|
+
) : ourExp;
|
|
2689
|
+
const claims = { sub, aud, iat, exp, obo: onBehalfOf?.token };
|
|
2690
|
+
const token = await new jose.SignJWT(claims).setProtectedHeader({
|
|
2691
|
+
typ: pluginAuthNode.tokenTypes.plugin.typParam,
|
|
2692
|
+
alg: this.algorithm,
|
|
2693
|
+
kid: key.kid
|
|
2694
|
+
}).setAudience(aud).setSubject(sub).setIssuedAt(iat).setExpirationTime(exp).sign(await jose.importJWK(key));
|
|
2695
|
+
return { token };
|
|
2696
|
+
}
|
|
2697
|
+
async isTargetPluginSupported(targetPluginId) {
|
|
2698
|
+
if (this.supportedTargetPlugins.has(targetPluginId)) {
|
|
2699
|
+
return true;
|
|
2343
2700
|
}
|
|
2344
|
-
|
|
2701
|
+
const inFlight = this.targetPluginInflightChecks.get(targetPluginId);
|
|
2702
|
+
if (inFlight) {
|
|
2703
|
+
return inFlight;
|
|
2704
|
+
}
|
|
2705
|
+
const doCheck = async () => {
|
|
2345
2706
|
try {
|
|
2346
|
-
await
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2707
|
+
const res = await fetch(
|
|
2708
|
+
`${await this.discovery.getBaseUrl(
|
|
2709
|
+
targetPluginId
|
|
2710
|
+
)}/.backstage/auth/v1/jwks.json`
|
|
2711
|
+
);
|
|
2712
|
+
if (res.status === 404) {
|
|
2713
|
+
return false;
|
|
2714
|
+
}
|
|
2715
|
+
if (!res.ok) {
|
|
2716
|
+
throw new Error(`Failed to fetch jwks.json, ${res.status}`);
|
|
2717
|
+
}
|
|
2718
|
+
const data = await res.json();
|
|
2719
|
+
if (!data.keys) {
|
|
2720
|
+
throw new Error(`Invalid jwks.json response, missing keys`);
|
|
2351
2721
|
}
|
|
2722
|
+
this.supportedTargetPlugins.add(targetPluginId);
|
|
2723
|
+
return true;
|
|
2724
|
+
} catch (error) {
|
|
2725
|
+
this.logger.error("Unexpected failure for target JWKS check", error);
|
|
2726
|
+
return false;
|
|
2727
|
+
} finally {
|
|
2728
|
+
this.targetPluginInflightChecks.delete(targetPluginId);
|
|
2352
2729
|
}
|
|
2353
|
-
}
|
|
2354
|
-
|
|
2730
|
+
};
|
|
2731
|
+
const check = doCheck();
|
|
2732
|
+
this.targetPluginInflightChecks.set(targetPluginId, check);
|
|
2733
|
+
return check;
|
|
2355
2734
|
}
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
const allAccessRestrictions = readAccessRestrictionsFromConfig(config);
|
|
2365
|
-
if (!token.match(/^\S+$/)) {
|
|
2366
|
-
throw new Error("Illegal token, must be a set of non-space characters");
|
|
2367
|
-
} else if (token.length < MIN_TOKEN_LENGTH) {
|
|
2368
|
-
throw new Error(
|
|
2369
|
-
`Illegal token, must be at least ${MIN_TOKEN_LENGTH} characters length`
|
|
2370
|
-
);
|
|
2371
|
-
} else if (!subject.match(/^\S+$/)) {
|
|
2372
|
-
throw new Error("Illegal subject, must be a set of non-space characters");
|
|
2373
|
-
} else if (this.#entries.has(token)) {
|
|
2374
|
-
throw new Error(
|
|
2375
|
-
"Static externalAccess token was declared more than once"
|
|
2735
|
+
async getJwksClient(pluginId) {
|
|
2736
|
+
const client = this.jwksMap.get(pluginId);
|
|
2737
|
+
if (client) {
|
|
2738
|
+
return client;
|
|
2739
|
+
}
|
|
2740
|
+
if (!await this.isTargetPluginSupported(pluginId)) {
|
|
2741
|
+
throw new errors.AuthenticationError(
|
|
2742
|
+
`Received a plugin token where the source '${pluginId}' plugin unexpectedly does not have a JWKS endpoint`
|
|
2376
2743
|
);
|
|
2377
2744
|
}
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2745
|
+
const newClient = new JwksClient(async () => {
|
|
2746
|
+
return new URL(
|
|
2747
|
+
`${await this.discovery.getBaseUrl(
|
|
2748
|
+
pluginId
|
|
2749
|
+
)}/.backstage/auth/v1/jwks.json`
|
|
2750
|
+
);
|
|
2751
|
+
});
|
|
2752
|
+
this.jwksMap.set(pluginId, newClient);
|
|
2753
|
+
return newClient;
|
|
2382
2754
|
}
|
|
2383
2755
|
}
|
|
2384
2756
|
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
);
|
|
2402
|
-
const subjectPrefix = config.getOptionalString("options.subjectPrefix");
|
|
2403
|
-
const url = new URL(config.getString("options.url"));
|
|
2404
|
-
const jwks = jose.createRemoteJWKSet(url);
|
|
2405
|
-
const allAccessRestrictions = readAccessRestrictionsFromConfig(config);
|
|
2406
|
-
this.#entries.push({
|
|
2407
|
-
algorithms,
|
|
2408
|
-
audiences,
|
|
2409
|
-
issuers,
|
|
2410
|
-
jwks,
|
|
2411
|
-
subjectPrefix,
|
|
2412
|
-
url,
|
|
2413
|
-
allAccessRestrictions
|
|
2414
|
-
});
|
|
2757
|
+
const MIGRATIONS_TABLE = "backstage_backend_public_keys__knex_migrations";
|
|
2758
|
+
const TABLE = "backstage_backend_public_keys__keys";
|
|
2759
|
+
function applyDatabaseMigrations(knex) {
|
|
2760
|
+
const migrationsDir = backendPluginApi.resolvePackagePath(
|
|
2761
|
+
"@backstage/backend-defaults",
|
|
2762
|
+
"migrations/auth"
|
|
2763
|
+
);
|
|
2764
|
+
return knex.migrate.latest({
|
|
2765
|
+
directory: migrationsDir,
|
|
2766
|
+
tableName: MIGRATIONS_TABLE
|
|
2767
|
+
});
|
|
2768
|
+
}
|
|
2769
|
+
class DatabaseKeyStore {
|
|
2770
|
+
constructor(client, logger) {
|
|
2771
|
+
this.client = client;
|
|
2772
|
+
this.logger = logger;
|
|
2415
2773
|
}
|
|
2416
|
-
async
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
} = await jose.jwtVerify(token, entry.jwks, {
|
|
2422
|
-
algorithms: entry.algorithms,
|
|
2423
|
-
issuer: entry.issuers,
|
|
2424
|
-
audience: entry.audiences
|
|
2425
|
-
});
|
|
2426
|
-
if (sub) {
|
|
2427
|
-
const prefix = entry.subjectPrefix ? `external:${entry.subjectPrefix}:` : "external:";
|
|
2428
|
-
return {
|
|
2429
|
-
subject: `${prefix}${sub}`,
|
|
2430
|
-
allAccessRestrictions: entry.allAccessRestrictions
|
|
2431
|
-
};
|
|
2432
|
-
}
|
|
2433
|
-
} catch {
|
|
2434
|
-
continue;
|
|
2435
|
-
}
|
|
2774
|
+
static async create(options) {
|
|
2775
|
+
const { database, logger } = options;
|
|
2776
|
+
const client = await database.getClient();
|
|
2777
|
+
if (!database.migrations?.skip) {
|
|
2778
|
+
await applyDatabaseMigrations(client);
|
|
2436
2779
|
}
|
|
2437
|
-
return
|
|
2780
|
+
return new DatabaseKeyStore(client, logger);
|
|
2438
2781
|
}
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
constructor(ownPluginId, handlers) {
|
|
2446
|
-
this.ownPluginId = ownPluginId;
|
|
2447
|
-
this.handlers = handlers;
|
|
2782
|
+
async addKey(options) {
|
|
2783
|
+
await this.client(TABLE).insert({
|
|
2784
|
+
id: options.key.kid,
|
|
2785
|
+
key: JSON.stringify(options.key),
|
|
2786
|
+
expires_at: options.expiresAt.toISOString()
|
|
2787
|
+
});
|
|
2448
2788
|
}
|
|
2449
|
-
|
|
2450
|
-
const
|
|
2451
|
-
const
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
if (!handler) {
|
|
2464
|
-
const valid = Object.keys(handlers).map((k) => `'${k}'`).join(", ");
|
|
2465
|
-
throw new Error(
|
|
2466
|
-
`Unknown type '${type}' in ${NEW_CONFIG_KEY}, expected one of ${valid}`
|
|
2467
|
-
);
|
|
2789
|
+
async listKeys() {
|
|
2790
|
+
const rows = await this.client(TABLE).select();
|
|
2791
|
+
const keys = rows.map((row) => ({
|
|
2792
|
+
id: row.id,
|
|
2793
|
+
key: JSON.parse(row.key),
|
|
2794
|
+
expiresAt: new Date(row.expires_at)
|
|
2795
|
+
}));
|
|
2796
|
+
const validKeys = [];
|
|
2797
|
+
const expiredKeys = [];
|
|
2798
|
+
for (const key of keys) {
|
|
2799
|
+
if (luxon.DateTime.fromJSDate(key.expiresAt) < luxon.DateTime.local()) {
|
|
2800
|
+
expiredKeys.push(key);
|
|
2801
|
+
} else {
|
|
2802
|
+
validKeys.push(key);
|
|
2468
2803
|
}
|
|
2469
|
-
handler.add(handlerConfig);
|
|
2470
2804
|
}
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
`DEPRECATION WARNING: The ${OLD_CONFIG_KEY} config has been replaced by ${NEW_CONFIG_KEY}, see https://backstage.io/docs/auth/service-to-service-auth`
|
|
2805
|
+
if (expiredKeys.length > 0) {
|
|
2806
|
+
const kids = expiredKeys.map(({ key }) => key.kid);
|
|
2807
|
+
this.logger.info(
|
|
2808
|
+
`Removing expired plugin service keys, '${kids.join("', '")}'`
|
|
2476
2809
|
);
|
|
2810
|
+
this.client(TABLE).delete().whereIn("id", kids).catch((error) => {
|
|
2811
|
+
this.logger.error(
|
|
2812
|
+
"Failed to remove expired plugin service keys",
|
|
2813
|
+
error
|
|
2814
|
+
);
|
|
2815
|
+
});
|
|
2477
2816
|
}
|
|
2478
|
-
|
|
2479
|
-
legacyHandler.addOld(handlerConfig);
|
|
2480
|
-
}
|
|
2481
|
-
return new ExternalTokenHandler(ownPluginId, Object.values(handlers));
|
|
2482
|
-
}
|
|
2483
|
-
async verifyToken(token) {
|
|
2484
|
-
for (const handler of this.handlers) {
|
|
2485
|
-
const result = await handler.verifyToken(token);
|
|
2486
|
-
if (result) {
|
|
2487
|
-
const { allAccessRestrictions, ...rest } = result;
|
|
2488
|
-
if (allAccessRestrictions) {
|
|
2489
|
-
const accessRestrictions = allAccessRestrictions.get(
|
|
2490
|
-
this.ownPluginId
|
|
2491
|
-
);
|
|
2492
|
-
if (!accessRestrictions) {
|
|
2493
|
-
const valid = [...allAccessRestrictions.keys()].map((k) => `'${k}'`).join(", ");
|
|
2494
|
-
throw new errors.NotAllowedError(
|
|
2495
|
-
`This token's access is restricted to plugin(s) ${valid}`
|
|
2496
|
-
);
|
|
2497
|
-
}
|
|
2498
|
-
return {
|
|
2499
|
-
...rest,
|
|
2500
|
-
accessRestrictions
|
|
2501
|
-
};
|
|
2502
|
-
}
|
|
2503
|
-
return rest;
|
|
2504
|
-
}
|
|
2505
|
-
}
|
|
2506
|
-
return void 0;
|
|
2817
|
+
return { keys: validKeys };
|
|
2507
2818
|
}
|
|
2508
2819
|
}
|
|
2509
2820
|
|
|
2510
|
-
const
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
const disableDefaultAuthPolicy = Boolean(
|
|
2526
|
-
config.getOptionalBoolean(
|
|
2527
|
-
"backend.auth.dangerouslyDisableDefaultAuthPolicy"
|
|
2528
|
-
)
|
|
2529
|
-
);
|
|
2530
|
-
const publicKeyStore = await DatabaseKeyStore.create({
|
|
2531
|
-
database,
|
|
2532
|
-
logger
|
|
2533
|
-
});
|
|
2534
|
-
const userTokens = UserTokenHandler.create({
|
|
2535
|
-
discovery
|
|
2536
|
-
});
|
|
2537
|
-
const pluginTokens = PluginTokenHandler.create({
|
|
2538
|
-
ownPluginId: plugin.getId(),
|
|
2539
|
-
keyDuration: { hours: 1 },
|
|
2540
|
-
logger,
|
|
2541
|
-
publicKeyStore,
|
|
2542
|
-
discovery
|
|
2543
|
-
});
|
|
2544
|
-
const externalTokens = ExternalTokenHandler.create({
|
|
2545
|
-
ownPluginId: plugin.getId(),
|
|
2546
|
-
config,
|
|
2547
|
-
logger
|
|
2821
|
+
const SECONDS_IN_MS$1 = 1e3;
|
|
2822
|
+
const KEY_EXPIRATION_MARGIN_FACTOR = 3;
|
|
2823
|
+
class DatabasePluginKeySource {
|
|
2824
|
+
constructor(keyStore, logger, keyDurationSeconds, algorithm) {
|
|
2825
|
+
this.keyStore = keyStore;
|
|
2826
|
+
this.logger = logger;
|
|
2827
|
+
this.keyDurationSeconds = keyDurationSeconds;
|
|
2828
|
+
this.algorithm = algorithm;
|
|
2829
|
+
}
|
|
2830
|
+
privateKeyPromise;
|
|
2831
|
+
keyExpiry;
|
|
2832
|
+
static async create(options) {
|
|
2833
|
+
const keyStore = await DatabaseKeyStore.create({
|
|
2834
|
+
database: options.database,
|
|
2835
|
+
logger: options.logger
|
|
2548
2836
|
});
|
|
2549
|
-
return new
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2837
|
+
return new DatabasePluginKeySource(
|
|
2838
|
+
keyStore,
|
|
2839
|
+
options.logger,
|
|
2840
|
+
Math.round(types.durationToMilliseconds(options.keyDuration) / 1e3),
|
|
2841
|
+
options.algorithm ?? "ES256"
|
|
2842
|
+
);
|
|
2843
|
+
}
|
|
2844
|
+
async getPrivateSigningKey() {
|
|
2845
|
+
if (this.privateKeyPromise) {
|
|
2846
|
+
if (this.keyExpiry && this.keyExpiry.getTime() > Date.now()) {
|
|
2847
|
+
return this.privateKeyPromise;
|
|
2848
|
+
}
|
|
2849
|
+
this.logger.info(`Signing key has expired, generating new key`);
|
|
2850
|
+
delete this.privateKeyPromise;
|
|
2851
|
+
}
|
|
2852
|
+
this.keyExpiry = new Date(
|
|
2853
|
+
Date.now() + this.keyDurationSeconds * SECONDS_IN_MS$1
|
|
2557
2854
|
);
|
|
2855
|
+
const promise = (async () => {
|
|
2856
|
+
const kid = uuid.v4();
|
|
2857
|
+
const key = await jose.generateKeyPair(this.algorithm);
|
|
2858
|
+
const publicKey = await jose.exportJWK(key.publicKey);
|
|
2859
|
+
const privateKey = await jose.exportJWK(key.privateKey);
|
|
2860
|
+
publicKey.kid = privateKey.kid = kid;
|
|
2861
|
+
publicKey.alg = privateKey.alg = this.algorithm;
|
|
2862
|
+
this.logger.info(`Created new signing key ${kid}`);
|
|
2863
|
+
await this.keyStore.addKey({
|
|
2864
|
+
id: kid,
|
|
2865
|
+
key: publicKey,
|
|
2866
|
+
expiresAt: new Date(
|
|
2867
|
+
Date.now() + this.keyDurationSeconds * SECONDS_IN_MS$1 * KEY_EXPIRATION_MARGIN_FACTOR
|
|
2868
|
+
)
|
|
2869
|
+
});
|
|
2870
|
+
return privateKey;
|
|
2871
|
+
})();
|
|
2872
|
+
this.privateKeyPromise = promise;
|
|
2873
|
+
try {
|
|
2874
|
+
await promise;
|
|
2875
|
+
} catch (error) {
|
|
2876
|
+
this.logger.error(`Failed to generate new signing key, ${error}`);
|
|
2877
|
+
delete this.keyExpiry;
|
|
2878
|
+
delete this.privateKeyPromise;
|
|
2879
|
+
}
|
|
2880
|
+
return promise;
|
|
2558
2881
|
}
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
const cacheServiceFactory = backendPluginApi.createServiceFactory({
|
|
2562
|
-
service: backendPluginApi.coreServices.cache,
|
|
2563
|
-
deps: {
|
|
2564
|
-
config: backendPluginApi.coreServices.rootConfig,
|
|
2565
|
-
logger: backendPluginApi.coreServices.rootLogger,
|
|
2566
|
-
plugin: backendPluginApi.coreServices.pluginMetadata
|
|
2567
|
-
},
|
|
2568
|
-
async createRootContext({ config, logger }) {
|
|
2569
|
-
return backendCommon.CacheManager.fromConfig(config, { logger });
|
|
2570
|
-
},
|
|
2571
|
-
async factory({ plugin }, manager) {
|
|
2572
|
-
return manager.forPlugin(plugin.getId()).getClient();
|
|
2882
|
+
listKeys() {
|
|
2883
|
+
return this.keyStore.listKeys();
|
|
2573
2884
|
}
|
|
2574
|
-
}
|
|
2885
|
+
}
|
|
2575
2886
|
|
|
2576
|
-
const
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2887
|
+
const DEFAULT_ALGORITHM = "ES256";
|
|
2888
|
+
const SECONDS_IN_MS = 1e3;
|
|
2889
|
+
class StaticConfigPluginKeySource {
|
|
2890
|
+
constructor(keyPairs, keyDurationSeconds) {
|
|
2891
|
+
this.keyPairs = keyPairs;
|
|
2892
|
+
this.keyDurationSeconds = keyDurationSeconds;
|
|
2893
|
+
}
|
|
2894
|
+
static async create(options) {
|
|
2895
|
+
const keyConfigs = options.sourceConfig.getConfigArray("static.keys").map((c) => {
|
|
2896
|
+
const staticKeyConfig = {
|
|
2897
|
+
publicKeyFile: c.getString("publicKeyFile"),
|
|
2898
|
+
privateKeyFile: c.getOptionalString("privateKeyFile"),
|
|
2899
|
+
keyId: c.getString("keyId"),
|
|
2900
|
+
algorithm: c.getOptionalString("algorithm") ?? DEFAULT_ALGORITHM
|
|
2901
|
+
};
|
|
2902
|
+
return staticKeyConfig;
|
|
2903
|
+
});
|
|
2904
|
+
const keyPairs = await Promise.all(
|
|
2905
|
+
keyConfigs.map(async (k) => await this.loadKeyPair(k))
|
|
2906
|
+
);
|
|
2907
|
+
if (keyPairs.length < 1) {
|
|
2908
|
+
throw new Error(
|
|
2909
|
+
"At least one key pair must be provided in static.keys, when the static key store type is used"
|
|
2910
|
+
);
|
|
2911
|
+
} else if (!keyPairs[0].privateKey) {
|
|
2912
|
+
throw new Error(
|
|
2913
|
+
"Private key for signing must be provided in the first key pair in static.keys, when the static key store type is used"
|
|
2914
|
+
);
|
|
2588
2915
|
}
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
const databaseServiceFactory = backendPluginApi.createServiceFactory({
|
|
2593
|
-
service: backendPluginApi.coreServices.database,
|
|
2594
|
-
deps: {
|
|
2595
|
-
config: backendPluginApi.coreServices.rootConfig,
|
|
2596
|
-
lifecycle: backendPluginApi.coreServices.lifecycle,
|
|
2597
|
-
pluginMetadata: backendPluginApi.coreServices.pluginMetadata
|
|
2598
|
-
},
|
|
2599
|
-
async createRootContext({ config: config$1 }) {
|
|
2600
|
-
return config$1.getOptional("backend.database") ? backendCommon.DatabaseManager.fromConfig(config$1) : backendCommon.DatabaseManager.fromConfig(
|
|
2601
|
-
new config.ConfigReader({
|
|
2602
|
-
backend: {
|
|
2603
|
-
database: { client: "better-sqlite3", connection: ":memory:" }
|
|
2604
|
-
}
|
|
2605
|
-
})
|
|
2916
|
+
return new StaticConfigPluginKeySource(
|
|
2917
|
+
keyPairs,
|
|
2918
|
+
types.durationToMilliseconds(options.keyDuration) / SECONDS_IN_MS
|
|
2606
2919
|
);
|
|
2607
|
-
}
|
|
2608
|
-
async
|
|
2609
|
-
return
|
|
2610
|
-
|
|
2611
|
-
|
|
2920
|
+
}
|
|
2921
|
+
async getPrivateSigningKey() {
|
|
2922
|
+
return this.keyPairs[0].privateKey;
|
|
2923
|
+
}
|
|
2924
|
+
async listKeys() {
|
|
2925
|
+
const keys = this.keyPairs.map((k) => this.keyPairToStoredKey(k));
|
|
2926
|
+
return { keys };
|
|
2927
|
+
}
|
|
2928
|
+
static async loadKeyPair(options) {
|
|
2929
|
+
const algorithm = options.algorithm;
|
|
2930
|
+
const keyId = options.keyId;
|
|
2931
|
+
const publicKey = await this.loadPublicKeyFromFile(
|
|
2932
|
+
options.publicKeyFile,
|
|
2933
|
+
keyId,
|
|
2934
|
+
algorithm
|
|
2935
|
+
);
|
|
2936
|
+
const privateKey = options.privateKeyFile ? await this.loadPrivateKeyFromFile(
|
|
2937
|
+
options.privateKeyFile,
|
|
2938
|
+
keyId,
|
|
2939
|
+
algorithm
|
|
2940
|
+
) : void 0;
|
|
2941
|
+
return { publicKey, privateKey, keyId };
|
|
2942
|
+
}
|
|
2943
|
+
static async loadPublicKeyFromFile(path, keyId, algorithm) {
|
|
2944
|
+
return this.loadKeyFromFile(path, keyId, algorithm, jose.importSPKI);
|
|
2945
|
+
}
|
|
2946
|
+
static async loadPrivateKeyFromFile(path, keyId, algorithm) {
|
|
2947
|
+
return this.loadKeyFromFile(path, keyId, algorithm, jose.importPKCS8);
|
|
2948
|
+
}
|
|
2949
|
+
static async loadKeyFromFile(path, keyId, algorithm, importer) {
|
|
2950
|
+
const content = await fs$1.promises.readFile(path, { encoding: "utf8", flag: "r" });
|
|
2951
|
+
const key = await importer(content, algorithm);
|
|
2952
|
+
const jwk = await jose.exportJWK(key);
|
|
2953
|
+
jwk.kid = keyId;
|
|
2954
|
+
jwk.alg = algorithm;
|
|
2955
|
+
return jwk;
|
|
2956
|
+
}
|
|
2957
|
+
keyPairToStoredKey(keyPair) {
|
|
2958
|
+
const publicKey = {
|
|
2959
|
+
...keyPair.publicKey,
|
|
2960
|
+
kid: keyPair.keyId
|
|
2961
|
+
};
|
|
2962
|
+
return {
|
|
2963
|
+
key: publicKey,
|
|
2964
|
+
id: keyPair.keyId,
|
|
2965
|
+
expiresAt: new Date(Date.now() + this.keyDurationSeconds * SECONDS_IN_MS)
|
|
2966
|
+
};
|
|
2967
|
+
}
|
|
2968
|
+
}
|
|
2969
|
+
|
|
2970
|
+
const CONFIG_ROOT_KEY = "backend.auth.pluginKeyStore";
|
|
2971
|
+
async function createPluginKeySource(options) {
|
|
2972
|
+
const keyStoreConfig = options.config.getOptionalConfig(CONFIG_ROOT_KEY);
|
|
2973
|
+
const type = keyStoreConfig?.getOptionalString("type") ?? "database";
|
|
2974
|
+
if (!keyStoreConfig || type === "database") {
|
|
2975
|
+
return DatabasePluginKeySource.create({
|
|
2976
|
+
database: options.database,
|
|
2977
|
+
logger: options.logger,
|
|
2978
|
+
keyDuration: options.keyDuration,
|
|
2979
|
+
algorithm: options.algorithm
|
|
2980
|
+
});
|
|
2981
|
+
} else if (type === "static") {
|
|
2982
|
+
return StaticConfigPluginKeySource.create({
|
|
2983
|
+
sourceConfig: keyStoreConfig,
|
|
2984
|
+
keyDuration: options.keyDuration
|
|
2612
2985
|
});
|
|
2613
2986
|
}
|
|
2614
|
-
|
|
2987
|
+
throw new Error(
|
|
2988
|
+
`Unsupported config value ${CONFIG_ROOT_KEY}.type '${type}'; expected one of 'database', 'static'`
|
|
2989
|
+
);
|
|
2990
|
+
}
|
|
2615
2991
|
|
|
2616
|
-
class
|
|
2617
|
-
constructor(
|
|
2618
|
-
this.
|
|
2619
|
-
this.externalBaseUrl = externalBaseUrl;
|
|
2620
|
-
this.discoveryConfig = discoveryConfig;
|
|
2992
|
+
class UserTokenHandler {
|
|
2993
|
+
constructor(jwksClient) {
|
|
2994
|
+
this.jwksClient = jwksClient;
|
|
2621
2995
|
}
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
* - target: https://internal.example.com/internal-catalog
|
|
2634
|
-
* plugins: [catalog]
|
|
2635
|
-
* - target: https://internal.example.com/secure/api/{{pluginId}}
|
|
2636
|
-
* plugins: [auth, permission]
|
|
2637
|
-
* - target:
|
|
2638
|
-
* internal: https://internal.example.com/search
|
|
2639
|
-
* external: https://example.com/search
|
|
2640
|
-
* plugins: [search]
|
|
2641
|
-
* ```
|
|
2642
|
-
*
|
|
2643
|
-
* The basePath defaults to `/api`, meaning the default full internal
|
|
2644
|
-
* path for the `catalog` plugin will be `http://localhost:7007/api/catalog`.
|
|
2645
|
-
*/
|
|
2646
|
-
static fromConfig(config, options) {
|
|
2647
|
-
const basePath = options?.basePath ?? "/api";
|
|
2648
|
-
const externalBaseUrl = config.getString("backend.baseUrl").replace(/\/+$/, "");
|
|
2649
|
-
const {
|
|
2650
|
-
listen: { host: listenHost = "::", port: listenPort }
|
|
2651
|
-
} = backendAppApi.readHttpServerOptions(config.getConfig("backend"));
|
|
2652
|
-
const protocol = config.has("backend.https") ? "https" : "http";
|
|
2653
|
-
let host = listenHost;
|
|
2654
|
-
if (host === "::" || host === "") {
|
|
2655
|
-
host = "localhost";
|
|
2656
|
-
} else if (host === "0.0.0.0") {
|
|
2657
|
-
host = "127.0.0.1";
|
|
2996
|
+
static create(options) {
|
|
2997
|
+
const jwksClient = new JwksClient(async () => {
|
|
2998
|
+
const url = await options.discovery.getBaseUrl("auth");
|
|
2999
|
+
return new URL(`${url}/.well-known/jwks.json`);
|
|
3000
|
+
});
|
|
3001
|
+
return new UserTokenHandler(jwksClient);
|
|
3002
|
+
}
|
|
3003
|
+
async verifyToken(token) {
|
|
3004
|
+
const verifyOpts = this.#getTokenVerificationOptions(token);
|
|
3005
|
+
if (!verifyOpts) {
|
|
3006
|
+
return void 0;
|
|
2658
3007
|
}
|
|
2659
|
-
|
|
2660
|
-
|
|
3008
|
+
await this.jwksClient.refreshKeyStore(token);
|
|
3009
|
+
const { payload } = await jose.jwtVerify(
|
|
3010
|
+
token,
|
|
3011
|
+
this.jwksClient.getKey,
|
|
3012
|
+
verifyOpts
|
|
3013
|
+
).catch((e) => {
|
|
3014
|
+
throw new errors.AuthenticationError("Invalid token", e);
|
|
3015
|
+
});
|
|
3016
|
+
const userEntityRef = payload.sub;
|
|
3017
|
+
if (!userEntityRef) {
|
|
3018
|
+
throw new errors.AuthenticationError("No user sub found in token");
|
|
2661
3019
|
}
|
|
2662
|
-
|
|
2663
|
-
return new HostDiscovery(
|
|
2664
|
-
internalBaseUrl + basePath,
|
|
2665
|
-
externalBaseUrl + basePath,
|
|
2666
|
-
config.getOptionalConfig("discovery")
|
|
2667
|
-
);
|
|
3020
|
+
return { userEntityRef };
|
|
2668
3021
|
}
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
3022
|
+
#getTokenVerificationOptions(token) {
|
|
3023
|
+
try {
|
|
3024
|
+
const { typ } = jose.decodeProtectedHeader(token);
|
|
3025
|
+
if (typ === pluginAuthNode.tokenTypes.user.typParam) {
|
|
3026
|
+
return {
|
|
3027
|
+
requiredClaims: ["iat", "exp", "sub"],
|
|
3028
|
+
typ: pluginAuthNode.tokenTypes.user.typParam
|
|
3029
|
+
};
|
|
3030
|
+
}
|
|
3031
|
+
if (typ === pluginAuthNode.tokenTypes.limitedUser.typParam) {
|
|
3032
|
+
return {
|
|
3033
|
+
requiredClaims: ["iat", "exp", "sub"],
|
|
3034
|
+
typ: pluginAuthNode.tokenTypes.limitedUser.typParam
|
|
3035
|
+
};
|
|
3036
|
+
}
|
|
3037
|
+
const { aud } = jose.decodeJwt(token);
|
|
3038
|
+
if (aud === pluginAuthNode.tokenTypes.user.audClaim) {
|
|
3039
|
+
return {
|
|
3040
|
+
audience: pluginAuthNode.tokenTypes.user.audClaim
|
|
3041
|
+
};
|
|
3042
|
+
}
|
|
3043
|
+
} catch {
|
|
2675
3044
|
}
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
3045
|
+
return void 0;
|
|
3046
|
+
}
|
|
3047
|
+
createLimitedUserToken(backstageToken) {
|
|
3048
|
+
const [headerRaw, payloadRaw] = backstageToken.split(".");
|
|
3049
|
+
const header = JSON.parse(
|
|
3050
|
+
new TextDecoder().decode(jose.base64url.decode(headerRaw))
|
|
3051
|
+
);
|
|
3052
|
+
const payload = JSON.parse(
|
|
3053
|
+
new TextDecoder().decode(jose.base64url.decode(payloadRaw))
|
|
3054
|
+
);
|
|
3055
|
+
const tokenType = header.typ;
|
|
3056
|
+
if (!tokenType || tokenType === pluginAuthNode.tokenTypes.limitedUser.typParam) {
|
|
3057
|
+
return { token: backstageToken, expiresAt: new Date(payload.exp * 1e3) };
|
|
3058
|
+
}
|
|
3059
|
+
if (tokenType !== pluginAuthNode.tokenTypes.user.typParam) {
|
|
3060
|
+
throw new errors.AuthenticationError(
|
|
3061
|
+
"Failed to create limited user token, invalid token type"
|
|
2680
3062
|
);
|
|
2681
3063
|
}
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
3064
|
+
const limitedUserToken = [
|
|
3065
|
+
jose.base64url.encode(
|
|
3066
|
+
JSON.stringify({
|
|
3067
|
+
typ: pluginAuthNode.tokenTypes.limitedUser.typParam,
|
|
3068
|
+
alg: header.alg,
|
|
3069
|
+
kid: header.kid
|
|
3070
|
+
})
|
|
3071
|
+
),
|
|
3072
|
+
jose.base64url.encode(
|
|
3073
|
+
JSON.stringify({
|
|
3074
|
+
sub: payload.sub,
|
|
3075
|
+
iat: payload.iat,
|
|
3076
|
+
exp: payload.exp
|
|
3077
|
+
})
|
|
3078
|
+
),
|
|
3079
|
+
payload.uip
|
|
3080
|
+
].join(".");
|
|
3081
|
+
return { token: limitedUserToken, expiresAt: new Date(payload.exp * 1e3) };
|
|
2689
3082
|
}
|
|
2690
|
-
|
|
2691
|
-
|
|
3083
|
+
isLimitedUserToken(token) {
|
|
3084
|
+
try {
|
|
3085
|
+
const { typ } = jose.decodeProtectedHeader(token);
|
|
3086
|
+
return typ === pluginAuthNode.tokenTypes.limitedUser.typParam;
|
|
3087
|
+
} catch {
|
|
3088
|
+
return false;
|
|
3089
|
+
}
|
|
2692
3090
|
}
|
|
2693
3091
|
}
|
|
2694
3092
|
|
|
2695
|
-
const
|
|
2696
|
-
service: backendPluginApi.coreServices.
|
|
3093
|
+
const authServiceFactory$1 = backendPluginApi.createServiceFactory({
|
|
3094
|
+
service: backendPluginApi.coreServices.auth,
|
|
2697
3095
|
deps: {
|
|
2698
|
-
config: backendPluginApi.coreServices.rootConfig
|
|
3096
|
+
config: backendPluginApi.coreServices.rootConfig,
|
|
3097
|
+
logger: backendPluginApi.coreServices.rootLogger,
|
|
3098
|
+
discovery: backendPluginApi.coreServices.discovery,
|
|
3099
|
+
plugin: backendPluginApi.coreServices.pluginMetadata,
|
|
3100
|
+
database: backendPluginApi.coreServices.database,
|
|
3101
|
+
// Re-using the token manager makes sure that we use the same generated keys for
|
|
3102
|
+
// development as plugins that have not yet been migrated. It's important that this
|
|
3103
|
+
// keeps working as long as there are plugins that have not been migrated to the
|
|
3104
|
+
// new auth services in the new backend system.
|
|
3105
|
+
tokenManager: backendPluginApi.coreServices.tokenManager
|
|
2699
3106
|
},
|
|
2700
|
-
async factory({ config }) {
|
|
2701
|
-
|
|
3107
|
+
async factory({ config, discovery, plugin, tokenManager, logger, database }) {
|
|
3108
|
+
const disableDefaultAuthPolicy = config.getOptionalBoolean(
|
|
3109
|
+
"backend.auth.dangerouslyDisableDefaultAuthPolicy"
|
|
3110
|
+
) ?? false;
|
|
3111
|
+
const keyDuration = { hours: 1 };
|
|
3112
|
+
const keySource = await createPluginKeySource({
|
|
3113
|
+
config,
|
|
3114
|
+
database,
|
|
3115
|
+
logger,
|
|
3116
|
+
keyDuration
|
|
3117
|
+
});
|
|
3118
|
+
const userTokens = UserTokenHandler.create({
|
|
3119
|
+
discovery
|
|
3120
|
+
});
|
|
3121
|
+
const pluginTokens = PluginTokenHandler.create({
|
|
3122
|
+
ownPluginId: plugin.getId(),
|
|
3123
|
+
logger,
|
|
3124
|
+
keySource,
|
|
3125
|
+
keyDuration,
|
|
3126
|
+
discovery
|
|
3127
|
+
});
|
|
3128
|
+
const externalTokens = ExternalTokenHandler.create({
|
|
3129
|
+
ownPluginId: plugin.getId(),
|
|
3130
|
+
config,
|
|
3131
|
+
logger
|
|
3132
|
+
});
|
|
3133
|
+
return new DefaultAuthService(
|
|
3134
|
+
userTokens,
|
|
3135
|
+
pluginTokens,
|
|
3136
|
+
externalTokens,
|
|
3137
|
+
tokenManager,
|
|
3138
|
+
plugin.getId(),
|
|
3139
|
+
disableDefaultAuthPolicy,
|
|
3140
|
+
keySource
|
|
3141
|
+
);
|
|
2702
3142
|
}
|
|
2703
3143
|
});
|
|
2704
3144
|
|
|
3145
|
+
const authServiceFactory = authServiceFactory$1;
|
|
3146
|
+
|
|
2705
3147
|
const FIVE_MINUTES_MS = 5 * 60 * 1e3;
|
|
2706
3148
|
const BACKSTAGE_AUTH_COOKIE = "backstage-auth";
|
|
2707
3149
|
function getTokenFromRequest(req) {
|
|
@@ -2874,7 +3316,7 @@ class DefaultHttpAuthService {
|
|
|
2874
3316
|
}
|
|
2875
3317
|
}
|
|
2876
3318
|
}
|
|
2877
|
-
const httpAuthServiceFactory = backendPluginApi.createServiceFactory({
|
|
3319
|
+
const httpAuthServiceFactory$1 = backendPluginApi.createServiceFactory({
|
|
2878
3320
|
service: backendPluginApi.coreServices.httpAuth,
|
|
2879
3321
|
deps: {
|
|
2880
3322
|
auth: backendPluginApi.coreServices.auth,
|
|
@@ -2886,8 +3328,10 @@ const httpAuthServiceFactory = backendPluginApi.createServiceFactory({
|
|
|
2886
3328
|
}
|
|
2887
3329
|
});
|
|
2888
3330
|
|
|
3331
|
+
const httpAuthServiceFactory = httpAuthServiceFactory$1;
|
|
3332
|
+
|
|
2889
3333
|
const DEFAULT_TIMEOUT = { seconds: 5 };
|
|
2890
|
-
function createLifecycleMiddleware(options) {
|
|
3334
|
+
function createLifecycleMiddleware$1(options) {
|
|
2891
3335
|
const { lifecycle, startupRequestPauseTimeout = DEFAULT_TIMEOUT } = options;
|
|
2892
3336
|
let state = "init";
|
|
2893
3337
|
const waiting = /* @__PURE__ */ new Set();
|
|
@@ -3011,7 +3455,7 @@ function createCookieAuthRefreshMiddleware(options) {
|
|
|
3011
3455
|
return router;
|
|
3012
3456
|
}
|
|
3013
3457
|
|
|
3014
|
-
const httpRouterServiceFactory = backendPluginApi.createServiceFactory(
|
|
3458
|
+
const httpRouterServiceFactory$1 = backendPluginApi.createServiceFactory(
|
|
3015
3459
|
(options) => ({
|
|
3016
3460
|
service: backendPluginApi.coreServices.httpRouter,
|
|
3017
3461
|
initialization: "always",
|
|
@@ -3047,7 +3491,7 @@ const httpRouterServiceFactory = backendPluginApi.createServiceFactory(
|
|
|
3047
3491
|
config
|
|
3048
3492
|
});
|
|
3049
3493
|
router.use(createAuthIntegrationRouter({ auth }));
|
|
3050
|
-
router.use(createLifecycleMiddleware({ lifecycle }));
|
|
3494
|
+
router.use(createLifecycleMiddleware$1({ lifecycle }));
|
|
3051
3495
|
router.use(credentialsBarrier.middleware);
|
|
3052
3496
|
router.use(createCookieAuthRefreshMiddleware({ auth, httpAuth }));
|
|
3053
3497
|
return {
|
|
@@ -3062,76 +3506,11 @@ const httpRouterServiceFactory = backendPluginApi.createServiceFactory(
|
|
|
3062
3506
|
})
|
|
3063
3507
|
);
|
|
3064
3508
|
|
|
3065
|
-
const
|
|
3066
|
-
(options) => ({
|
|
3067
|
-
service: backendPluginApi.coreServices.identity,
|
|
3068
|
-
deps: {
|
|
3069
|
-
discovery: backendPluginApi.coreServices.discovery
|
|
3070
|
-
},
|
|
3071
|
-
async factory({ discovery }) {
|
|
3072
|
-
return pluginAuthNode.DefaultIdentityClient.create({ discovery, ...options });
|
|
3073
|
-
}
|
|
3074
|
-
})
|
|
3075
|
-
);
|
|
3509
|
+
const httpRouterServiceFactory = httpRouterServiceFactory$1;
|
|
3076
3510
|
|
|
3077
|
-
|
|
3078
|
-
constructor(logger, rootLifecycle, pluginMetadata) {
|
|
3079
|
-
this.logger = logger;
|
|
3080
|
-
this.rootLifecycle = rootLifecycle;
|
|
3081
|
-
this.pluginMetadata = pluginMetadata;
|
|
3082
|
-
}
|
|
3083
|
-
#hasStarted = false;
|
|
3084
|
-
#startupTasks = [];
|
|
3085
|
-
addStartupHook(hook, options) {
|
|
3086
|
-
if (this.#hasStarted) {
|
|
3087
|
-
throw new Error("Attempted to add startup hook after startup");
|
|
3088
|
-
}
|
|
3089
|
-
this.#startupTasks.push({ hook, options });
|
|
3090
|
-
}
|
|
3091
|
-
async startup() {
|
|
3092
|
-
if (this.#hasStarted) {
|
|
3093
|
-
return;
|
|
3094
|
-
}
|
|
3095
|
-
this.#hasStarted = true;
|
|
3096
|
-
this.logger.debug(
|
|
3097
|
-
`Running ${this.#startupTasks.length} plugin startup tasks...`
|
|
3098
|
-
);
|
|
3099
|
-
await Promise.all(
|
|
3100
|
-
this.#startupTasks.map(async ({ hook, options }) => {
|
|
3101
|
-
const logger = options?.logger ?? this.logger;
|
|
3102
|
-
try {
|
|
3103
|
-
await hook();
|
|
3104
|
-
logger.debug(`Plugin startup hook succeeded`);
|
|
3105
|
-
} catch (error) {
|
|
3106
|
-
logger.error(`Plugin startup hook failed, ${error}`);
|
|
3107
|
-
}
|
|
3108
|
-
})
|
|
3109
|
-
);
|
|
3110
|
-
}
|
|
3111
|
-
addShutdownHook(hook, options) {
|
|
3112
|
-
const plugin = this.pluginMetadata.getId();
|
|
3113
|
-
this.rootLifecycle.addShutdownHook(hook, {
|
|
3114
|
-
logger: options?.logger?.child({ plugin }) ?? this.logger
|
|
3115
|
-
});
|
|
3116
|
-
}
|
|
3117
|
-
}
|
|
3118
|
-
const lifecycleServiceFactory = backendPluginApi.createServiceFactory({
|
|
3119
|
-
service: backendPluginApi.coreServices.lifecycle,
|
|
3120
|
-
deps: {
|
|
3121
|
-
logger: backendPluginApi.coreServices.logger,
|
|
3122
|
-
rootLifecycle: backendPluginApi.coreServices.rootLifecycle,
|
|
3123
|
-
pluginMetadata: backendPluginApi.coreServices.pluginMetadata
|
|
3124
|
-
},
|
|
3125
|
-
async factory({ rootLifecycle, logger, pluginMetadata }) {
|
|
3126
|
-
return new BackendPluginLifecycleImpl(
|
|
3127
|
-
logger,
|
|
3128
|
-
rootLifecycle,
|
|
3129
|
-
pluginMetadata
|
|
3130
|
-
);
|
|
3131
|
-
}
|
|
3132
|
-
});
|
|
3511
|
+
const createLifecycleMiddleware = createLifecycleMiddleware$1;
|
|
3133
3512
|
|
|
3134
|
-
const loggerServiceFactory = backendPluginApi.createServiceFactory({
|
|
3513
|
+
const loggerServiceFactory$1 = backendPluginApi.createServiceFactory({
|
|
3135
3514
|
service: backendPluginApi.coreServices.logger,
|
|
3136
3515
|
deps: {
|
|
3137
3516
|
rootLogger: backendPluginApi.coreServices.rootLogger,
|
|
@@ -3142,27 +3521,12 @@ const loggerServiceFactory = backendPluginApi.createServiceFactory({
|
|
|
3142
3521
|
}
|
|
3143
3522
|
});
|
|
3144
3523
|
|
|
3145
|
-
const
|
|
3146
|
-
service: backendPluginApi.coreServices.permissions,
|
|
3147
|
-
deps: {
|
|
3148
|
-
auth: backendPluginApi.coreServices.auth,
|
|
3149
|
-
config: backendPluginApi.coreServices.rootConfig,
|
|
3150
|
-
discovery: backendPluginApi.coreServices.discovery,
|
|
3151
|
-
tokenManager: backendPluginApi.coreServices.tokenManager
|
|
3152
|
-
},
|
|
3153
|
-
async factory({ auth, config, discovery, tokenManager }) {
|
|
3154
|
-
return pluginPermissionNode.ServerPermissionClient.fromConfig(config, {
|
|
3155
|
-
auth,
|
|
3156
|
-
discovery,
|
|
3157
|
-
tokenManager
|
|
3158
|
-
});
|
|
3159
|
-
}
|
|
3160
|
-
});
|
|
3524
|
+
const loggerServiceFactory = loggerServiceFactory$1;
|
|
3161
3525
|
|
|
3162
3526
|
function normalizePath(path) {
|
|
3163
3527
|
return `${trimEnd__default.default(path, "/")}/`;
|
|
3164
3528
|
}
|
|
3165
|
-
class DefaultRootHttpRouter {
|
|
3529
|
+
let DefaultRootHttpRouter$1 = class DefaultRootHttpRouter {
|
|
3166
3530
|
#indexPath;
|
|
3167
3531
|
#router = express.Router();
|
|
3168
3532
|
#namedRoutes = express.Router();
|
|
@@ -3223,12 +3587,12 @@ class DefaultRootHttpRouter {
|
|
|
3223
3587
|
}
|
|
3224
3588
|
return void 0;
|
|
3225
3589
|
}
|
|
3226
|
-
}
|
|
3590
|
+
};
|
|
3227
3591
|
|
|
3228
3592
|
function defaultConfigure({ applyDefaults }) {
|
|
3229
3593
|
applyDefaults();
|
|
3230
3594
|
}
|
|
3231
|
-
const rootHttpRouterServiceFactory = backendPluginApi.createServiceFactory(
|
|
3595
|
+
const rootHttpRouterServiceFactory$1 = backendPluginApi.createServiceFactory(
|
|
3232
3596
|
(options) => ({
|
|
3233
3597
|
service: backendPluginApi.coreServices.rootHttpRouter,
|
|
3234
3598
|
deps: {
|
|
@@ -3240,12 +3604,12 @@ const rootHttpRouterServiceFactory = backendPluginApi.createServiceFactory(
|
|
|
3240
3604
|
const { indexPath, configure = defaultConfigure } = options ?? {};
|
|
3241
3605
|
const logger = rootLogger.child({ service: "rootHttpRouter" });
|
|
3242
3606
|
const app = express__default.default();
|
|
3243
|
-
const router = DefaultRootHttpRouter.create({ indexPath });
|
|
3244
|
-
const middleware = MiddlewareFactory.create({ config, logger });
|
|
3607
|
+
const router = DefaultRootHttpRouter$1.create({ indexPath });
|
|
3608
|
+
const middleware = MiddlewareFactory$1.create({ config, logger });
|
|
3245
3609
|
const routes = router.handler();
|
|
3246
|
-
const server = await createHttpServer(
|
|
3610
|
+
const server = await createHttpServer$1(
|
|
3247
3611
|
app,
|
|
3248
|
-
readHttpServerOptions(config.getOptionalConfig("backend")),
|
|
3612
|
+
readHttpServerOptions$1(config.getOptionalConfig("backend")),
|
|
3249
3613
|
{ logger }
|
|
3250
3614
|
);
|
|
3251
3615
|
configure({
|
|
@@ -3273,128 +3637,46 @@ const rootHttpRouterServiceFactory = backendPluginApi.createServiceFactory(
|
|
|
3273
3637
|
})
|
|
3274
3638
|
);
|
|
3275
3639
|
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
#startupTasks = [];
|
|
3282
|
-
addStartupHook(hook, options) {
|
|
3283
|
-
if (this.#hasStarted) {
|
|
3284
|
-
throw new Error("Attempted to add startup hook after startup");
|
|
3285
|
-
}
|
|
3286
|
-
this.#startupTasks.push({ hook, options });
|
|
3640
|
+
const rootHttpRouterServiceFactory = rootHttpRouterServiceFactory$1;
|
|
3641
|
+
|
|
3642
|
+
class DefaultRootHttpRouter {
|
|
3643
|
+
constructor(impl) {
|
|
3644
|
+
this.impl = impl;
|
|
3287
3645
|
}
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
return;
|
|
3291
|
-
}
|
|
3292
|
-
this.#hasStarted = true;
|
|
3293
|
-
this.logger.debug(`Running ${this.#startupTasks.length} startup tasks...`);
|
|
3294
|
-
await Promise.all(
|
|
3295
|
-
this.#startupTasks.map(async ({ hook, options }) => {
|
|
3296
|
-
const logger = options?.logger ?? this.logger;
|
|
3297
|
-
try {
|
|
3298
|
-
await hook();
|
|
3299
|
-
logger.debug(`Startup hook succeeded`);
|
|
3300
|
-
} catch (error) {
|
|
3301
|
-
logger.error(`Startup hook failed, ${error}`);
|
|
3302
|
-
}
|
|
3303
|
-
})
|
|
3304
|
-
);
|
|
3646
|
+
static create(options) {
|
|
3647
|
+
return new DefaultRootHttpRouter(DefaultRootHttpRouter$1.create(options));
|
|
3305
3648
|
}
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
addShutdownHook(hook, options) {
|
|
3309
|
-
if (this.#hasShutdown) {
|
|
3310
|
-
throw new Error("Attempted to add shutdown hook after shutdown");
|
|
3311
|
-
}
|
|
3312
|
-
this.#shutdownTasks.push({ hook, options });
|
|
3649
|
+
use(path, handler) {
|
|
3650
|
+
this.impl.use(path, handler);
|
|
3313
3651
|
}
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
return;
|
|
3317
|
-
}
|
|
3318
|
-
this.#hasShutdown = true;
|
|
3319
|
-
this.logger.debug(
|
|
3320
|
-
`Running ${this.#shutdownTasks.length} shutdown tasks...`
|
|
3321
|
-
);
|
|
3322
|
-
await Promise.all(
|
|
3323
|
-
this.#shutdownTasks.map(async ({ hook, options }) => {
|
|
3324
|
-
const logger = options?.logger ?? this.logger;
|
|
3325
|
-
try {
|
|
3326
|
-
await hook();
|
|
3327
|
-
logger.debug(`Shutdown hook succeeded`);
|
|
3328
|
-
} catch (error) {
|
|
3329
|
-
logger.error(`Shutdown hook failed, ${error}`);
|
|
3330
|
-
}
|
|
3331
|
-
})
|
|
3332
|
-
);
|
|
3652
|
+
handler() {
|
|
3653
|
+
return this.impl.handler();
|
|
3333
3654
|
}
|
|
3334
3655
|
}
|
|
3335
|
-
const rootLifecycleServiceFactory = backendPluginApi.createServiceFactory({
|
|
3336
|
-
service: backendPluginApi.coreServices.rootLifecycle,
|
|
3337
|
-
deps: {
|
|
3338
|
-
logger: backendPluginApi.coreServices.rootLogger
|
|
3339
|
-
},
|
|
3340
|
-
async factory({ logger }) {
|
|
3341
|
-
return new BackendLifecycleImpl(logger);
|
|
3342
|
-
}
|
|
3343
|
-
});
|
|
3344
|
-
|
|
3345
|
-
const rootLoggerServiceFactory = backendPluginApi.createServiceFactory({
|
|
3346
|
-
service: backendPluginApi.coreServices.rootLogger,
|
|
3347
|
-
deps: {
|
|
3348
|
-
config: backendPluginApi.coreServices.rootConfig
|
|
3349
|
-
},
|
|
3350
|
-
async factory({ config }) {
|
|
3351
|
-
const logger = WinstonLogger.create({
|
|
3352
|
-
meta: {
|
|
3353
|
-
service: "backstage"
|
|
3354
|
-
},
|
|
3355
|
-
level: process.env.LOG_LEVEL || "info",
|
|
3356
|
-
format: process.env.NODE_ENV === "production" ? winston.format.json() : WinstonLogger.colorFormat(),
|
|
3357
|
-
transports: [new winston.transports.Console()]
|
|
3358
|
-
});
|
|
3359
|
-
const secretEnumerator = await createConfigSecretEnumerator({ logger });
|
|
3360
|
-
logger.addRedactions(secretEnumerator(config));
|
|
3361
|
-
config.subscribe?.(() => logger.addRedactions(secretEnumerator(config)));
|
|
3362
|
-
return logger;
|
|
3363
|
-
}
|
|
3364
|
-
});
|
|
3365
3656
|
|
|
3366
|
-
const
|
|
3367
|
-
service: backendPluginApi.coreServices.tokenManager,
|
|
3368
|
-
deps: {
|
|
3369
|
-
config: backendPluginApi.coreServices.rootConfig,
|
|
3370
|
-
logger: backendPluginApi.coreServices.rootLogger
|
|
3371
|
-
},
|
|
3372
|
-
createRootContext({ config, logger }) {
|
|
3373
|
-
return backendCommon.ServerTokenManager.fromConfig(config, {
|
|
3374
|
-
logger,
|
|
3375
|
-
allowDisabledTokenManager: true
|
|
3376
|
-
});
|
|
3377
|
-
},
|
|
3378
|
-
async factory(_deps, tokenManager) {
|
|
3379
|
-
return tokenManager;
|
|
3380
|
-
}
|
|
3381
|
-
});
|
|
3657
|
+
const rootLoggerServiceFactory = rootLoggerServiceFactory$1;
|
|
3382
3658
|
|
|
3383
|
-
const
|
|
3384
|
-
service: backendPluginApi.coreServices.
|
|
3659
|
+
const schedulerServiceFactory = backendPluginApi.createServiceFactory({
|
|
3660
|
+
service: backendPluginApi.coreServices.scheduler,
|
|
3385
3661
|
deps: {
|
|
3386
|
-
|
|
3662
|
+
plugin: backendPluginApi.coreServices.pluginMetadata,
|
|
3663
|
+
databaseManager: backendPluginApi.coreServices.database,
|
|
3387
3664
|
logger: backendPluginApi.coreServices.logger
|
|
3388
3665
|
},
|
|
3389
|
-
async factory({
|
|
3390
|
-
return
|
|
3391
|
-
|
|
3666
|
+
async factory({ plugin, databaseManager, logger }) {
|
|
3667
|
+
return backendTasks.TaskScheduler.forPlugin({
|
|
3668
|
+
pluginId: plugin.getId(),
|
|
3669
|
+
databaseManager,
|
|
3392
3670
|
logger
|
|
3393
3671
|
});
|
|
3394
3672
|
}
|
|
3395
3673
|
});
|
|
3396
3674
|
|
|
3397
3675
|
class DefaultUserInfoService {
|
|
3676
|
+
discovery;
|
|
3677
|
+
constructor(options) {
|
|
3678
|
+
this.discovery = options.discovery;
|
|
3679
|
+
}
|
|
3398
3680
|
async getUserInfo(credentials) {
|
|
3399
3681
|
const internalCredentials = toInternalBackstageCredentials(credentials);
|
|
3400
3682
|
if (internalCredentials.principal.type !== "user") {
|
|
@@ -3403,42 +3685,51 @@ class DefaultUserInfoService {
|
|
|
3403
3685
|
if (!internalCredentials.token) {
|
|
3404
3686
|
throw new Error("User credentials is unexpectedly missing token");
|
|
3405
3687
|
}
|
|
3406
|
-
const { sub: userEntityRef, ent:
|
|
3688
|
+
const { sub: userEntityRef, ent: tokenEnt } = jose.decodeJwt(
|
|
3407
3689
|
internalCredentials.token
|
|
3408
3690
|
);
|
|
3409
3691
|
if (typeof userEntityRef !== "string") {
|
|
3410
3692
|
throw new Error("User entity ref must be a string");
|
|
3411
3693
|
}
|
|
3412
|
-
|
|
3694
|
+
let ownershipEntityRefs = tokenEnt;
|
|
3695
|
+
if (!ownershipEntityRefs) {
|
|
3696
|
+
const userInfoResp = await fetch__default.default(
|
|
3697
|
+
`${await this.discovery.getBaseUrl("auth")}/v1/userinfo`,
|
|
3698
|
+
{
|
|
3699
|
+
headers: {
|
|
3700
|
+
Authorization: `Bearer ${internalCredentials.token}`
|
|
3701
|
+
}
|
|
3702
|
+
}
|
|
3703
|
+
);
|
|
3704
|
+
if (!userInfoResp.ok) {
|
|
3705
|
+
throw await errors.ResponseError.fromResponse(userInfoResp);
|
|
3706
|
+
}
|
|
3707
|
+
const {
|
|
3708
|
+
claims: { ent }
|
|
3709
|
+
} = await userInfoResp.json();
|
|
3710
|
+
ownershipEntityRefs = ent;
|
|
3711
|
+
}
|
|
3712
|
+
if (!ownershipEntityRefs) {
|
|
3713
|
+
throw new Error("Ownership entity refs can not be determined");
|
|
3714
|
+
} else if (!Array.isArray(ownershipEntityRefs) || ownershipEntityRefs.some((ref) => typeof ref !== "string")) {
|
|
3413
3715
|
throw new Error("Ownership entity refs must be an array of strings");
|
|
3414
3716
|
}
|
|
3415
3717
|
return { userEntityRef, ownershipEntityRefs };
|
|
3416
3718
|
}
|
|
3417
3719
|
}
|
|
3418
|
-
const userInfoServiceFactory = backendPluginApi.createServiceFactory({
|
|
3419
|
-
service: backendPluginApi.coreServices.userInfo,
|
|
3420
|
-
deps: {},
|
|
3421
|
-
async factory() {
|
|
3422
|
-
return new DefaultUserInfoService();
|
|
3423
|
-
}
|
|
3424
|
-
});
|
|
3425
3720
|
|
|
3426
|
-
const
|
|
3427
|
-
service: backendPluginApi.coreServices.
|
|
3721
|
+
const userInfoServiceFactory$1 = backendPluginApi.createServiceFactory({
|
|
3722
|
+
service: backendPluginApi.coreServices.userInfo,
|
|
3428
3723
|
deps: {
|
|
3429
|
-
|
|
3430
|
-
databaseManager: backendPluginApi.coreServices.database,
|
|
3431
|
-
logger: backendPluginApi.coreServices.logger
|
|
3724
|
+
discovery: backendPluginApi.coreServices.discovery
|
|
3432
3725
|
},
|
|
3433
|
-
async factory({
|
|
3434
|
-
return
|
|
3435
|
-
pluginId: plugin.getId(),
|
|
3436
|
-
databaseManager,
|
|
3437
|
-
logger
|
|
3438
|
-
});
|
|
3726
|
+
async factory({ discovery }) {
|
|
3727
|
+
return new DefaultUserInfoService({ discovery });
|
|
3439
3728
|
}
|
|
3440
3729
|
});
|
|
3441
3730
|
|
|
3731
|
+
const userInfoServiceFactory = userInfoServiceFactory$1;
|
|
3732
|
+
|
|
3442
3733
|
exports.DefaultRootHttpRouter = DefaultRootHttpRouter;
|
|
3443
3734
|
exports.HostDiscovery = HostDiscovery;
|
|
3444
3735
|
exports.MiddlewareFactory = MiddlewareFactory;
|