@backstage/backend-app-api 0.7.6-next.3 → 0.7.7
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 +87 -0
- package/alpha/package.json +2 -2
- 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 +1516 -1124
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +333 -86
- 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,118 @@ 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
|
+
class MiddlewareFactory {
|
|
774
|
+
constructor(impl) {
|
|
775
|
+
this.impl = impl;
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Creates a new {@link MiddlewareFactory}.
|
|
779
|
+
*/
|
|
780
|
+
static create(options) {
|
|
781
|
+
return MiddlewareFactory$1.create(options);
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Returns a middleware that unconditionally produces a 404 error response.
|
|
785
|
+
*
|
|
786
|
+
* @remarks
|
|
787
|
+
*
|
|
788
|
+
* Typically you want to place this middleware at the end of the chain, such
|
|
789
|
+
* that it's the last one attempted after no other routes matched.
|
|
790
|
+
*
|
|
791
|
+
* @returns An Express request handler
|
|
792
|
+
*/
|
|
793
|
+
notFound() {
|
|
794
|
+
return this.impl.notFound();
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* Returns the compression middleware.
|
|
798
|
+
*
|
|
799
|
+
* @remarks
|
|
800
|
+
*
|
|
801
|
+
* The middleware will attempt to compress response bodies for all requests
|
|
802
|
+
* that traverse through the middleware.
|
|
803
|
+
*/
|
|
804
|
+
compression() {
|
|
805
|
+
return this.impl.compression();
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Returns a request logging middleware.
|
|
809
|
+
*
|
|
810
|
+
* @remarks
|
|
811
|
+
*
|
|
812
|
+
* Typically you want to place this middleware at the start of the chain, such
|
|
813
|
+
* that it always logs requests whether they are "caught" by handlers farther
|
|
814
|
+
* down or not.
|
|
815
|
+
*
|
|
816
|
+
* @returns An Express request handler
|
|
817
|
+
*/
|
|
818
|
+
logging() {
|
|
819
|
+
return this.impl.logging();
|
|
820
|
+
}
|
|
821
|
+
/**
|
|
822
|
+
* Returns a middleware that implements the helmet library.
|
|
823
|
+
*
|
|
824
|
+
* @remarks
|
|
825
|
+
*
|
|
826
|
+
* This middleware applies security policies to incoming requests and outgoing
|
|
827
|
+
* responses. It is configured using config keys such as `backend.csp`.
|
|
828
|
+
*
|
|
829
|
+
* @see {@link https://helmetjs.github.io/}
|
|
830
|
+
*
|
|
831
|
+
* @returns An Express request handler
|
|
832
|
+
*/
|
|
833
|
+
helmet() {
|
|
834
|
+
return this.impl.helmet();
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* Returns a middleware that implements the cors library.
|
|
838
|
+
*
|
|
839
|
+
* @remarks
|
|
840
|
+
*
|
|
841
|
+
* This middleware handles CORS. It is configured using the config key
|
|
842
|
+
* `backend.cors`.
|
|
843
|
+
*
|
|
844
|
+
* @see {@link https://github.com/expressjs/cors}
|
|
845
|
+
*
|
|
846
|
+
* @returns An Express request handler
|
|
847
|
+
*/
|
|
848
|
+
cors() {
|
|
849
|
+
return this.impl.cors();
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* Express middleware to handle errors during request processing.
|
|
853
|
+
*
|
|
854
|
+
* @remarks
|
|
855
|
+
*
|
|
856
|
+
* This is commonly the very last middleware in the chain.
|
|
857
|
+
*
|
|
858
|
+
* Its primary purpose is not to do translation of business logic exceptions,
|
|
859
|
+
* but rather to be a global catch-all for uncaught "fatal" errors that are
|
|
860
|
+
* expected to result in a 500 error. However, it also does handle some common
|
|
861
|
+
* error types (such as http-error exceptions, and the well-known error types
|
|
862
|
+
* in the `@backstage/errors` package) and returns the enclosed status code
|
|
863
|
+
* accordingly.
|
|
864
|
+
*
|
|
865
|
+
* It will also produce a response body with a serialized form of the error,
|
|
866
|
+
* unless a previous handler already did send a body. See
|
|
867
|
+
* {@link @backstage/errors#ErrorResponseBody} for the response shape used.
|
|
868
|
+
*
|
|
869
|
+
* @returns An Express error request handler
|
|
870
|
+
*/
|
|
871
|
+
error(options = {}) {
|
|
872
|
+
return this.impl.error(options);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
765
876
|
const escapeRegExp = (text) => {
|
|
766
877
|
return text.replace(/[.*+?^${}(\)|[\]\\]/g, "\\$&");
|
|
767
878
|
};
|
|
768
879
|
|
|
769
|
-
class WinstonLogger {
|
|
880
|
+
let WinstonLogger$1 = class WinstonLogger {
|
|
770
881
|
#winston;
|
|
771
882
|
#addRedactions;
|
|
772
883
|
/**
|
|
@@ -870,6 +981,69 @@ class WinstonLogger {
|
|
|
870
981
|
addRedactions(redactions) {
|
|
871
982
|
this.#addRedactions?.(redactions);
|
|
872
983
|
}
|
|
984
|
+
};
|
|
985
|
+
|
|
986
|
+
const rootLoggerServiceFactory$1 = backendPluginApi.createServiceFactory({
|
|
987
|
+
service: backendPluginApi.coreServices.rootLogger,
|
|
988
|
+
deps: {
|
|
989
|
+
config: backendPluginApi.coreServices.rootConfig
|
|
990
|
+
},
|
|
991
|
+
async factory({ config }) {
|
|
992
|
+
const logger = WinstonLogger$1.create({
|
|
993
|
+
meta: {
|
|
994
|
+
service: "backstage"
|
|
995
|
+
},
|
|
996
|
+
level: process.env.LOG_LEVEL || "info",
|
|
997
|
+
format: process.env.NODE_ENV === "production" ? winston.format.json() : WinstonLogger$1.colorFormat(),
|
|
998
|
+
transports: [new winston.transports.Console()]
|
|
999
|
+
});
|
|
1000
|
+
const secretEnumerator = await createConfigSecretEnumerator$1({ logger });
|
|
1001
|
+
logger.addRedactions(secretEnumerator(config));
|
|
1002
|
+
config.subscribe?.(() => logger.addRedactions(secretEnumerator(config)));
|
|
1003
|
+
return logger;
|
|
1004
|
+
}
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
class WinstonLogger {
|
|
1008
|
+
constructor(impl) {
|
|
1009
|
+
this.impl = impl;
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Creates a {@link WinstonLogger} instance.
|
|
1013
|
+
*/
|
|
1014
|
+
static create(options) {
|
|
1015
|
+
return new WinstonLogger(WinstonLogger$1.create(options));
|
|
1016
|
+
}
|
|
1017
|
+
/**
|
|
1018
|
+
* Creates a winston log formatter for redacting secrets.
|
|
1019
|
+
*/
|
|
1020
|
+
static redacter() {
|
|
1021
|
+
return WinstonLogger$1.redacter();
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
1024
|
+
* Creates a pretty printed winston log formatter.
|
|
1025
|
+
*/
|
|
1026
|
+
static colorFormat() {
|
|
1027
|
+
return WinstonLogger$1.colorFormat();
|
|
1028
|
+
}
|
|
1029
|
+
error(message, meta) {
|
|
1030
|
+
this.impl.error(message, meta);
|
|
1031
|
+
}
|
|
1032
|
+
warn(message, meta) {
|
|
1033
|
+
this.impl.warn(message, meta);
|
|
1034
|
+
}
|
|
1035
|
+
info(message, meta) {
|
|
1036
|
+
this.impl.info(message, meta);
|
|
1037
|
+
}
|
|
1038
|
+
debug(message, meta) {
|
|
1039
|
+
this.impl.debug(message, meta);
|
|
1040
|
+
}
|
|
1041
|
+
child(meta) {
|
|
1042
|
+
return this.impl.child(meta);
|
|
1043
|
+
}
|
|
1044
|
+
addRedactions(redactions) {
|
|
1045
|
+
this.impl.addRedactions(redactions);
|
|
1046
|
+
}
|
|
873
1047
|
}
|
|
874
1048
|
|
|
875
1049
|
class Node {
|
|
@@ -1633,69 +1807,384 @@ function createSpecializedBackend(options) {
|
|
|
1633
1807
|
return new BackstageBackend(services);
|
|
1634
1808
|
}
|
|
1635
1809
|
|
|
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;
|
|
1810
|
+
const cacheServiceFactory = backendPluginApi.createServiceFactory({
|
|
1811
|
+
service: backendPluginApi.coreServices.cache,
|
|
1812
|
+
deps: {
|
|
1813
|
+
config: backendPluginApi.coreServices.rootConfig,
|
|
1814
|
+
logger: backendPluginApi.coreServices.rootLogger,
|
|
1815
|
+
plugin: backendPluginApi.coreServices.pluginMetadata
|
|
1816
|
+
},
|
|
1817
|
+
async createRootContext({ config, logger }) {
|
|
1818
|
+
return backendCommon.CacheManager.fromConfig(config, { logger });
|
|
1819
|
+
},
|
|
1820
|
+
async factory({ plugin }, manager) {
|
|
1821
|
+
return manager.forPlugin(plugin.getId()).getClient();
|
|
1652
1822
|
}
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1823
|
+
});
|
|
1824
|
+
|
|
1825
|
+
const rootConfigServiceFactory = backendPluginApi.createServiceFactory(
|
|
1826
|
+
(options) => ({
|
|
1827
|
+
service: backendPluginApi.coreServices.rootConfig,
|
|
1828
|
+
deps: {},
|
|
1829
|
+
async factory() {
|
|
1830
|
+
const source = configLoader.ConfigSources.default({
|
|
1831
|
+
argv: options?.argv,
|
|
1832
|
+
remote: options?.remote,
|
|
1833
|
+
watch: options?.watch
|
|
1834
|
+
});
|
|
1835
|
+
console.log(`Loading config from ${source}`);
|
|
1836
|
+
return await configLoader.ConfigSources.toConfig(source);
|
|
1658
1837
|
}
|
|
1659
|
-
|
|
1838
|
+
})
|
|
1839
|
+
);
|
|
1840
|
+
|
|
1841
|
+
const databaseServiceFactory = backendPluginApi.createServiceFactory({
|
|
1842
|
+
service: backendPluginApi.coreServices.database,
|
|
1843
|
+
deps: {
|
|
1844
|
+
config: backendPluginApi.coreServices.rootConfig,
|
|
1845
|
+
lifecycle: backendPluginApi.coreServices.lifecycle,
|
|
1846
|
+
pluginMetadata: backendPluginApi.coreServices.pluginMetadata
|
|
1847
|
+
},
|
|
1848
|
+
async createRootContext({ config: config$1 }) {
|
|
1849
|
+
return config$1.getOptional("backend.database") ? backendCommon.DatabaseManager.fromConfig(config$1) : backendCommon.DatabaseManager.fromConfig(
|
|
1850
|
+
new config.ConfigReader({
|
|
1851
|
+
backend: {
|
|
1852
|
+
database: { client: "better-sqlite3", connection: ":memory:" }
|
|
1853
|
+
}
|
|
1854
|
+
})
|
|
1855
|
+
);
|
|
1856
|
+
},
|
|
1857
|
+
async factory({ pluginMetadata, lifecycle }, databaseManager) {
|
|
1858
|
+
return databaseManager.forPlugin(pluginMetadata.getId(), {
|
|
1859
|
+
pluginMetadata,
|
|
1860
|
+
lifecycle
|
|
1861
|
+
});
|
|
1660
1862
|
}
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1863
|
+
});
|
|
1864
|
+
|
|
1865
|
+
let HostDiscovery$1 = class HostDiscovery {
|
|
1866
|
+
constructor(internalBaseUrl, externalBaseUrl, discoveryConfig) {
|
|
1867
|
+
this.internalBaseUrl = internalBaseUrl;
|
|
1868
|
+
this.externalBaseUrl = externalBaseUrl;
|
|
1869
|
+
this.discoveryConfig = discoveryConfig;
|
|
1870
|
+
}
|
|
1871
|
+
/**
|
|
1872
|
+
* Creates a new HostDiscovery discovery instance by reading
|
|
1873
|
+
* from the `backend` config section, specifically the `.baseUrl` for
|
|
1874
|
+
* discovering the external URL, and the `.listen` and `.https` config
|
|
1875
|
+
* for the internal one.
|
|
1876
|
+
*
|
|
1877
|
+
* Can be overridden in config by providing a target and corresponding plugins in `discovery.endpoints`.
|
|
1878
|
+
* eg.
|
|
1879
|
+
* ```yaml
|
|
1880
|
+
* discovery:
|
|
1881
|
+
* endpoints:
|
|
1882
|
+
* - target: https://internal.example.com/internal-catalog
|
|
1883
|
+
* plugins: [catalog]
|
|
1884
|
+
* - target: https://internal.example.com/secure/api/{{pluginId}}
|
|
1885
|
+
* plugins: [auth, permission]
|
|
1886
|
+
* - target:
|
|
1887
|
+
* internal: https://internal.example.com/search
|
|
1888
|
+
* external: https://example.com/search
|
|
1889
|
+
* plugins: [search]
|
|
1890
|
+
* ```
|
|
1891
|
+
*
|
|
1892
|
+
* The basePath defaults to `/api`, meaning the default full internal
|
|
1893
|
+
* path for the `catalog` plugin will be `http://localhost:7007/api/catalog`.
|
|
1894
|
+
*/
|
|
1895
|
+
static fromConfig(config, options) {
|
|
1896
|
+
const basePath = options?.basePath ?? "/api";
|
|
1897
|
+
const externalBaseUrl = config.getString("backend.baseUrl").replace(/\/+$/, "");
|
|
1898
|
+
const {
|
|
1899
|
+
listen: { host: listenHost = "::", port: listenPort }
|
|
1900
|
+
} = readHttpServerOptions$1(config.getConfig("backend"));
|
|
1901
|
+
const protocol = config.has("backend.https") ? "https" : "http";
|
|
1902
|
+
let host = listenHost;
|
|
1903
|
+
if (host === "::" || host === "") {
|
|
1904
|
+
host = "localhost";
|
|
1905
|
+
} else if (host === "0.0.0.0") {
|
|
1906
|
+
host = "127.0.0.1";
|
|
1907
|
+
}
|
|
1908
|
+
if (host.includes(":")) {
|
|
1909
|
+
host = `[${host}]`;
|
|
1910
|
+
}
|
|
1911
|
+
const internalBaseUrl = `${protocol}://${host}:${listenPort}`;
|
|
1912
|
+
return new HostDiscovery(
|
|
1913
|
+
internalBaseUrl + basePath,
|
|
1914
|
+
externalBaseUrl + basePath,
|
|
1915
|
+
config.getOptionalConfig("discovery")
|
|
1916
|
+
);
|
|
1917
|
+
}
|
|
1918
|
+
getTargetFromConfig(pluginId, type) {
|
|
1919
|
+
const endpoints = this.discoveryConfig?.getOptionalConfigArray("endpoints");
|
|
1920
|
+
const target = endpoints?.find((endpoint) => endpoint.getStringArray("plugins").includes(pluginId))?.get("target");
|
|
1921
|
+
if (!target) {
|
|
1922
|
+
const baseUrl = type === "external" ? this.externalBaseUrl : this.internalBaseUrl;
|
|
1923
|
+
return `${baseUrl}/${encodeURIComponent(pluginId)}`;
|
|
1924
|
+
}
|
|
1925
|
+
if (typeof target === "string") {
|
|
1926
|
+
return target.replace(
|
|
1927
|
+
/\{\{\s*pluginId\s*\}\}/g,
|
|
1928
|
+
encodeURIComponent(pluginId)
|
|
1929
|
+
);
|
|
1930
|
+
}
|
|
1931
|
+
return target[type].replace(
|
|
1932
|
+
/\{\{\s*pluginId\s*\}\}/g,
|
|
1933
|
+
encodeURIComponent(pluginId)
|
|
1934
|
+
);
|
|
1935
|
+
}
|
|
1936
|
+
async getBaseUrl(pluginId) {
|
|
1937
|
+
return this.getTargetFromConfig(pluginId, "internal");
|
|
1938
|
+
}
|
|
1939
|
+
async getExternalBaseUrl(pluginId) {
|
|
1940
|
+
return this.getTargetFromConfig(pluginId, "external");
|
|
1941
|
+
}
|
|
1942
|
+
};
|
|
1943
|
+
|
|
1944
|
+
backendPluginApi.createServiceFactory({
|
|
1945
|
+
service: backendPluginApi.coreServices.discovery,
|
|
1946
|
+
deps: {
|
|
1947
|
+
config: backendPluginApi.coreServices.rootConfig
|
|
1948
|
+
},
|
|
1949
|
+
async factory({ config }) {
|
|
1950
|
+
return HostDiscovery$1.fromConfig(config);
|
|
1951
|
+
}
|
|
1952
|
+
});
|
|
1953
|
+
|
|
1954
|
+
class HostDiscovery {
|
|
1955
|
+
constructor(impl) {
|
|
1956
|
+
this.impl = impl;
|
|
1957
|
+
}
|
|
1958
|
+
/**
|
|
1959
|
+
* Creates a new HostDiscovery discovery instance by reading
|
|
1960
|
+
* from the `backend` config section, specifically the `.baseUrl` for
|
|
1961
|
+
* discovering the external URL, and the `.listen` and `.https` config
|
|
1962
|
+
* for the internal one.
|
|
1963
|
+
*
|
|
1964
|
+
* Can be overridden in config by providing a target and corresponding plugins in `discovery.endpoints`.
|
|
1965
|
+
* eg.
|
|
1966
|
+
* ```yaml
|
|
1967
|
+
* discovery:
|
|
1968
|
+
* endpoints:
|
|
1969
|
+
* - target: https://internal.example.com/internal-catalog
|
|
1970
|
+
* plugins: [catalog]
|
|
1971
|
+
* - target: https://internal.example.com/secure/api/{{pluginId}}
|
|
1972
|
+
* plugins: [auth, permission]
|
|
1973
|
+
* - target:
|
|
1974
|
+
* internal: https://internal.example.com/search
|
|
1975
|
+
* external: https://example.com/search
|
|
1976
|
+
* plugins: [search]
|
|
1977
|
+
* ```
|
|
1978
|
+
*
|
|
1979
|
+
* The basePath defaults to `/api`, meaning the default full internal
|
|
1980
|
+
* path for the `catalog` plugin will be `http://localhost:7007/api/catalog`.
|
|
1981
|
+
*/
|
|
1982
|
+
static fromConfig(config, options) {
|
|
1983
|
+
return new HostDiscovery(HostDiscovery$1.fromConfig(config, options));
|
|
1984
|
+
}
|
|
1985
|
+
async getBaseUrl(pluginId) {
|
|
1986
|
+
return this.impl.getBaseUrl(pluginId);
|
|
1987
|
+
}
|
|
1988
|
+
async getExternalBaseUrl(pluginId) {
|
|
1989
|
+
return this.impl.getExternalBaseUrl(pluginId);
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
const discoveryServiceFactory = backendPluginApi.createServiceFactory({
|
|
1994
|
+
service: backendPluginApi.coreServices.discovery,
|
|
1995
|
+
deps: {
|
|
1996
|
+
config: backendPluginApi.coreServices.rootConfig
|
|
1997
|
+
},
|
|
1998
|
+
async factory({ config }) {
|
|
1999
|
+
return HostDiscovery.fromConfig(config);
|
|
2000
|
+
}
|
|
2001
|
+
});
|
|
2002
|
+
|
|
2003
|
+
const identityServiceFactory = backendPluginApi.createServiceFactory(
|
|
2004
|
+
(options) => ({
|
|
2005
|
+
service: backendPluginApi.coreServices.identity,
|
|
2006
|
+
deps: {
|
|
2007
|
+
discovery: backendPluginApi.coreServices.discovery
|
|
2008
|
+
},
|
|
2009
|
+
async factory({ discovery }) {
|
|
2010
|
+
return pluginAuthNode.DefaultIdentityClient.create({ discovery, ...options });
|
|
2011
|
+
}
|
|
2012
|
+
})
|
|
2013
|
+
);
|
|
2014
|
+
|
|
2015
|
+
class BackendPluginLifecycleImpl {
|
|
2016
|
+
constructor(logger, rootLifecycle, pluginMetadata) {
|
|
2017
|
+
this.logger = logger;
|
|
2018
|
+
this.rootLifecycle = rootLifecycle;
|
|
2019
|
+
this.pluginMetadata = pluginMetadata;
|
|
2020
|
+
}
|
|
2021
|
+
#hasStarted = false;
|
|
2022
|
+
#startupTasks = [];
|
|
2023
|
+
addStartupHook(hook, options) {
|
|
2024
|
+
if (this.#hasStarted) {
|
|
2025
|
+
throw new Error("Attempted to add startup hook after startup");
|
|
2026
|
+
}
|
|
2027
|
+
this.#startupTasks.push({ hook, options });
|
|
2028
|
+
}
|
|
2029
|
+
async startup() {
|
|
2030
|
+
if (this.#hasStarted) {
|
|
2031
|
+
return;
|
|
2032
|
+
}
|
|
2033
|
+
this.#hasStarted = true;
|
|
2034
|
+
this.logger.debug(
|
|
2035
|
+
`Running ${this.#startupTasks.length} plugin startup tasks...`
|
|
2036
|
+
);
|
|
2037
|
+
await Promise.all(
|
|
2038
|
+
this.#startupTasks.map(async ({ hook, options }) => {
|
|
2039
|
+
const logger = options?.logger ?? this.logger;
|
|
2040
|
+
try {
|
|
2041
|
+
await hook();
|
|
2042
|
+
logger.debug(`Plugin startup hook succeeded`);
|
|
2043
|
+
} catch (error) {
|
|
2044
|
+
logger.error(`Plugin startup hook failed, ${error}`);
|
|
2045
|
+
}
|
|
2046
|
+
})
|
|
2047
|
+
);
|
|
2048
|
+
}
|
|
2049
|
+
addShutdownHook(hook, options) {
|
|
2050
|
+
const plugin = this.pluginMetadata.getId();
|
|
2051
|
+
this.rootLifecycle.addShutdownHook(hook, {
|
|
2052
|
+
logger: options?.logger?.child({ plugin }) ?? this.logger
|
|
2053
|
+
});
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
const lifecycleServiceFactory = backendPluginApi.createServiceFactory({
|
|
2057
|
+
service: backendPluginApi.coreServices.lifecycle,
|
|
2058
|
+
deps: {
|
|
2059
|
+
logger: backendPluginApi.coreServices.logger,
|
|
2060
|
+
rootLifecycle: backendPluginApi.coreServices.rootLifecycle,
|
|
2061
|
+
pluginMetadata: backendPluginApi.coreServices.pluginMetadata
|
|
2062
|
+
},
|
|
2063
|
+
async factory({ rootLifecycle, logger, pluginMetadata }) {
|
|
2064
|
+
return new BackendPluginLifecycleImpl(
|
|
2065
|
+
logger,
|
|
2066
|
+
rootLifecycle,
|
|
2067
|
+
pluginMetadata
|
|
2068
|
+
);
|
|
2069
|
+
}
|
|
2070
|
+
});
|
|
2071
|
+
|
|
2072
|
+
const permissionsServiceFactory = backendPluginApi.createServiceFactory({
|
|
2073
|
+
service: backendPluginApi.coreServices.permissions,
|
|
2074
|
+
deps: {
|
|
2075
|
+
auth: backendPluginApi.coreServices.auth,
|
|
2076
|
+
config: backendPluginApi.coreServices.rootConfig,
|
|
2077
|
+
discovery: backendPluginApi.coreServices.discovery,
|
|
2078
|
+
tokenManager: backendPluginApi.coreServices.tokenManager
|
|
2079
|
+
},
|
|
2080
|
+
async factory({ auth, config, discovery, tokenManager }) {
|
|
2081
|
+
return pluginPermissionNode.ServerPermissionClient.fromConfig(config, {
|
|
2082
|
+
auth,
|
|
2083
|
+
discovery,
|
|
2084
|
+
tokenManager
|
|
2085
|
+
});
|
|
2086
|
+
}
|
|
2087
|
+
});
|
|
2088
|
+
|
|
2089
|
+
class BackendLifecycleImpl {
|
|
2090
|
+
constructor(logger) {
|
|
2091
|
+
this.logger = logger;
|
|
2092
|
+
}
|
|
2093
|
+
#hasStarted = false;
|
|
2094
|
+
#startupTasks = [];
|
|
2095
|
+
addStartupHook(hook, options) {
|
|
2096
|
+
if (this.#hasStarted) {
|
|
2097
|
+
throw new Error("Attempted to add startup hook after startup");
|
|
2098
|
+
}
|
|
2099
|
+
this.#startupTasks.push({ hook, options });
|
|
2100
|
+
}
|
|
2101
|
+
async startup() {
|
|
2102
|
+
if (this.#hasStarted) {
|
|
2103
|
+
return;
|
|
2104
|
+
}
|
|
2105
|
+
this.#hasStarted = true;
|
|
2106
|
+
this.logger.debug(`Running ${this.#startupTasks.length} startup tasks...`);
|
|
2107
|
+
await Promise.all(
|
|
2108
|
+
this.#startupTasks.map(async ({ hook, options }) => {
|
|
2109
|
+
const logger = options?.logger ?? this.logger;
|
|
2110
|
+
try {
|
|
2111
|
+
await hook();
|
|
2112
|
+
logger.debug(`Startup hook succeeded`);
|
|
2113
|
+
} catch (error) {
|
|
2114
|
+
logger.error(`Startup hook failed, ${error}`);
|
|
2115
|
+
}
|
|
2116
|
+
})
|
|
2117
|
+
);
|
|
2118
|
+
}
|
|
2119
|
+
#hasShutdown = false;
|
|
2120
|
+
#shutdownTasks = [];
|
|
2121
|
+
addShutdownHook(hook, options) {
|
|
2122
|
+
if (this.#hasShutdown) {
|
|
2123
|
+
throw new Error("Attempted to add shutdown hook after shutdown");
|
|
2124
|
+
}
|
|
2125
|
+
this.#shutdownTasks.push({ hook, options });
|
|
2126
|
+
}
|
|
2127
|
+
async shutdown() {
|
|
2128
|
+
if (this.#hasShutdown) {
|
|
2129
|
+
return;
|
|
2130
|
+
}
|
|
2131
|
+
this.#hasShutdown = true;
|
|
2132
|
+
this.logger.debug(
|
|
2133
|
+
`Running ${this.#shutdownTasks.length} shutdown tasks...`
|
|
2134
|
+
);
|
|
2135
|
+
await Promise.all(
|
|
2136
|
+
this.#shutdownTasks.map(async ({ hook, options }) => {
|
|
2137
|
+
const logger = options?.logger ?? this.logger;
|
|
2138
|
+
try {
|
|
2139
|
+
await hook();
|
|
2140
|
+
logger.debug(`Shutdown hook succeeded`);
|
|
2141
|
+
} catch (error) {
|
|
2142
|
+
logger.error(`Shutdown hook failed, ${error}`);
|
|
2143
|
+
}
|
|
2144
|
+
})
|
|
2145
|
+
);
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
const rootLifecycleServiceFactory = backendPluginApi.createServiceFactory({
|
|
2149
|
+
service: backendPluginApi.coreServices.rootLifecycle,
|
|
2150
|
+
deps: {
|
|
2151
|
+
logger: backendPluginApi.coreServices.rootLogger
|
|
2152
|
+
},
|
|
2153
|
+
async factory({ logger }) {
|
|
2154
|
+
return new BackendLifecycleImpl(logger);
|
|
2155
|
+
}
|
|
2156
|
+
});
|
|
2157
|
+
|
|
2158
|
+
const tokenManagerServiceFactory = backendPluginApi.createServiceFactory({
|
|
2159
|
+
service: backendPluginApi.coreServices.tokenManager,
|
|
2160
|
+
deps: {
|
|
2161
|
+
config: backendPluginApi.coreServices.rootConfig,
|
|
2162
|
+
logger: backendPluginApi.coreServices.rootLogger
|
|
2163
|
+
},
|
|
2164
|
+
createRootContext({ config, logger }) {
|
|
2165
|
+
return backendCommon.ServerTokenManager.fromConfig(config, {
|
|
2166
|
+
logger,
|
|
2167
|
+
allowDisabledTokenManager: true
|
|
1666
2168
|
});
|
|
2169
|
+
},
|
|
2170
|
+
async factory(_deps, tokenManager) {
|
|
2171
|
+
return tokenManager;
|
|
1667
2172
|
}
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
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
|
-
);
|
|
1694
|
-
});
|
|
1695
|
-
}
|
|
1696
|
-
return { keys: validKeys };
|
|
2173
|
+
});
|
|
2174
|
+
|
|
2175
|
+
const urlReaderServiceFactory = backendPluginApi.createServiceFactory({
|
|
2176
|
+
service: backendPluginApi.coreServices.urlReader,
|
|
2177
|
+
deps: {
|
|
2178
|
+
config: backendPluginApi.coreServices.rootConfig,
|
|
2179
|
+
logger: backendPluginApi.coreServices.logger
|
|
2180
|
+
},
|
|
2181
|
+
async factory({ config, logger }) {
|
|
2182
|
+
return backendCommon.UrlReaders.default({
|
|
2183
|
+
config,
|
|
2184
|
+
logger
|
|
2185
|
+
});
|
|
1697
2186
|
}
|
|
1698
|
-
}
|
|
2187
|
+
});
|
|
1699
2188
|
|
|
1700
2189
|
function createCredentialsWithServicePrincipal(sub, token, accessRestrictions) {
|
|
1701
2190
|
return {
|
|
@@ -1744,17 +2233,16 @@ function toInternalBackstageCredentials(credentials) {
|
|
|
1744
2233
|
}
|
|
1745
2234
|
|
|
1746
2235
|
class DefaultAuthService {
|
|
1747
|
-
constructor(userTokenHandler, pluginTokenHandler, externalTokenHandler, tokenManager, pluginId, disableDefaultAuthPolicy,
|
|
2236
|
+
constructor(userTokenHandler, pluginTokenHandler, externalTokenHandler, tokenManager, pluginId, disableDefaultAuthPolicy, pluginKeySource) {
|
|
1748
2237
|
this.userTokenHandler = userTokenHandler;
|
|
1749
2238
|
this.pluginTokenHandler = pluginTokenHandler;
|
|
1750
2239
|
this.externalTokenHandler = externalTokenHandler;
|
|
1751
2240
|
this.tokenManager = tokenManager;
|
|
1752
2241
|
this.pluginId = pluginId;
|
|
1753
2242
|
this.disableDefaultAuthPolicy = disableDefaultAuthPolicy;
|
|
1754
|
-
this.
|
|
2243
|
+
this.pluginKeySource = pluginKeySource;
|
|
1755
2244
|
}
|
|
1756
|
-
|
|
1757
|
-
async authenticate(token) {
|
|
2245
|
+
async authenticate(token, options) {
|
|
1758
2246
|
const pluginResult = await this.pluginTokenHandler.verifyToken(token);
|
|
1759
2247
|
if (pluginResult) {
|
|
1760
2248
|
if (pluginResult.limitedUserToken) {
|
|
@@ -1776,6 +2264,9 @@ class DefaultAuthService {
|
|
|
1776
2264
|
}
|
|
1777
2265
|
const userResult = await this.userTokenHandler.verifyToken(token);
|
|
1778
2266
|
if (userResult) {
|
|
2267
|
+
if (!options?.allowLimitedAccess && this.userTokenHandler.isLimitedUserToken(token)) {
|
|
2268
|
+
throw new errors.AuthenticationError("Illegal limited user token");
|
|
2269
|
+
}
|
|
1779
2270
|
return createCredentialsWithUserPrincipal(
|
|
1780
2271
|
userResult.userEntityRef,
|
|
1781
2272
|
token,
|
|
@@ -1788,920 +2279,972 @@ class DefaultAuthService {
|
|
|
1788
2279
|
externalResult.subject,
|
|
1789
2280
|
void 0,
|
|
1790
2281
|
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 };
|
|
2282
|
+
);
|
|
2283
|
+
}
|
|
2284
|
+
throw new errors.AuthenticationError("Illegal token");
|
|
2004
2285
|
}
|
|
2005
|
-
|
|
2006
|
-
|
|
2286
|
+
isPrincipal(credentials, type) {
|
|
2287
|
+
const principal = credentials.principal;
|
|
2288
|
+
if (type === "unknown") {
|
|
2007
2289
|
return true;
|
|
2008
2290
|
}
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
return inFlight;
|
|
2291
|
+
if (principal.type !== type) {
|
|
2292
|
+
return false;
|
|
2012
2293
|
}
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2294
|
+
return true;
|
|
2295
|
+
}
|
|
2296
|
+
async getNoneCredentials() {
|
|
2297
|
+
return createCredentialsWithNonePrincipal();
|
|
2298
|
+
}
|
|
2299
|
+
async getOwnServiceCredentials() {
|
|
2300
|
+
return createCredentialsWithServicePrincipal(`plugin:${this.pluginId}`);
|
|
2301
|
+
}
|
|
2302
|
+
async getPluginRequestToken(options) {
|
|
2303
|
+
const { targetPluginId } = options;
|
|
2304
|
+
const internalForward = toInternalBackstageCredentials(options.onBehalfOf);
|
|
2305
|
+
const { type } = internalForward.principal;
|
|
2306
|
+
if (type === "none" && this.disableDefaultAuthPolicy) {
|
|
2307
|
+
return { token: "" };
|
|
2308
|
+
}
|
|
2309
|
+
const targetSupportsNewAuth = await this.pluginTokenHandler.isTargetPluginSupported(targetPluginId);
|
|
2310
|
+
switch (type) {
|
|
2311
|
+
case "service":
|
|
2312
|
+
if (targetSupportsNewAuth) {
|
|
2313
|
+
return this.pluginTokenHandler.issueToken({
|
|
2314
|
+
pluginId: this.pluginId,
|
|
2017
2315
|
targetPluginId
|
|
2018
|
-
)
|
|
2019
|
-
);
|
|
2020
|
-
if (res.status === 404) {
|
|
2021
|
-
return false;
|
|
2316
|
+
});
|
|
2022
2317
|
}
|
|
2023
|
-
|
|
2024
|
-
throw new
|
|
2318
|
+
return this.tokenManager.getToken().catch((error) => {
|
|
2319
|
+
throw new errors.ForwardedError(
|
|
2320
|
+
`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`,
|
|
2321
|
+
error
|
|
2322
|
+
);
|
|
2323
|
+
});
|
|
2324
|
+
case "user": {
|
|
2325
|
+
const { token } = internalForward;
|
|
2326
|
+
if (!token) {
|
|
2327
|
+
throw new Error("User credentials is unexpectedly missing token");
|
|
2025
2328
|
}
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2329
|
+
if (targetSupportsNewAuth) {
|
|
2330
|
+
const onBehalfOf = await this.userTokenHandler.createLimitedUserToken(
|
|
2331
|
+
token
|
|
2332
|
+
);
|
|
2333
|
+
return this.pluginTokenHandler.issueToken({
|
|
2334
|
+
pluginId: this.pluginId,
|
|
2335
|
+
targetPluginId,
|
|
2336
|
+
onBehalfOf
|
|
2337
|
+
});
|
|
2029
2338
|
}
|
|
2030
|
-
this.
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
this.targetPluginInflightChecks.delete(targetPluginId);
|
|
2339
|
+
if (this.userTokenHandler.isLimitedUserToken(token)) {
|
|
2340
|
+
throw new errors.AuthenticationError(
|
|
2341
|
+
`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`
|
|
2342
|
+
);
|
|
2343
|
+
}
|
|
2344
|
+
return { token };
|
|
2037
2345
|
}
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
}
|
|
2043
|
-
async getJwksClient(pluginId) {
|
|
2044
|
-
const client = this.jwksMap.get(pluginId);
|
|
2045
|
-
if (client) {
|
|
2046
|
-
return client;
|
|
2346
|
+
default:
|
|
2347
|
+
throw new errors.AuthenticationError(
|
|
2348
|
+
`Refused to issue service token for credential type '${type}'`
|
|
2349
|
+
);
|
|
2047
2350
|
}
|
|
2048
|
-
|
|
2351
|
+
}
|
|
2352
|
+
async getLimitedUserToken(credentials) {
|
|
2353
|
+
const { token: backstageToken } = toInternalBackstageCredentials(credentials);
|
|
2354
|
+
if (!backstageToken) {
|
|
2049
2355
|
throw new errors.AuthenticationError(
|
|
2050
|
-
|
|
2356
|
+
"User credentials is unexpectedly missing token"
|
|
2051
2357
|
);
|
|
2052
2358
|
}
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2359
|
+
return this.userTokenHandler.createLimitedUserToken(backstageToken);
|
|
2360
|
+
}
|
|
2361
|
+
async listPublicServiceKeys() {
|
|
2362
|
+
const { keys } = await this.pluginKeySource.listKeys();
|
|
2363
|
+
return { keys: keys.map(({ key }) => key) };
|
|
2364
|
+
}
|
|
2365
|
+
#getJwtExpiration(token) {
|
|
2366
|
+
const { exp } = jose.decodeJwt(token);
|
|
2367
|
+
if (!exp) {
|
|
2368
|
+
throw new errors.AuthenticationError("User token is missing expiration");
|
|
2369
|
+
}
|
|
2370
|
+
return new Date(exp * 1e3);
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
function readAccessRestrictionsFromConfig(externalAccessEntryConfig) {
|
|
2375
|
+
const configs = externalAccessEntryConfig.getOptionalConfigArray("accessRestrictions") ?? [];
|
|
2376
|
+
const result = /* @__PURE__ */ new Map();
|
|
2377
|
+
for (const config of configs) {
|
|
2378
|
+
const validKeys = ["plugin", "permission", "permissionAttribute"];
|
|
2379
|
+
for (const key of config.keys()) {
|
|
2380
|
+
if (!validKeys.includes(key)) {
|
|
2381
|
+
const valid = validKeys.map((k) => `'${k}'`).join(", ");
|
|
2382
|
+
throw new Error(
|
|
2383
|
+
`Invalid key '${key}' in 'accessRestrictions' config, expected one of ${valid}`
|
|
2384
|
+
);
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
const pluginId = config.getString("plugin");
|
|
2388
|
+
const permissionNames = readPermissionNames(config);
|
|
2389
|
+
const permissionAttributes = readPermissionAttributes(config);
|
|
2390
|
+
if (result.has(pluginId)) {
|
|
2391
|
+
throw new Error(
|
|
2392
|
+
`Attempted to declare 'accessRestrictions' twice for plugin '${pluginId}', which is not permitted`
|
|
2058
2393
|
);
|
|
2394
|
+
}
|
|
2395
|
+
result.set(pluginId, {
|
|
2396
|
+
...permissionNames ? { permissionNames } : {},
|
|
2397
|
+
...permissionAttributes ? { permissionAttributes } : {}
|
|
2059
2398
|
});
|
|
2060
|
-
this.jwksMap.set(pluginId, newClient);
|
|
2061
|
-
return newClient;
|
|
2062
2399
|
}
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2400
|
+
return result.size ? result : void 0;
|
|
2401
|
+
}
|
|
2402
|
+
function readStringOrStringArrayFromConfig(root, key, validValues) {
|
|
2403
|
+
if (!root.has(key)) {
|
|
2404
|
+
return void 0;
|
|
2405
|
+
}
|
|
2406
|
+
const rawValues = Array.isArray(root.get(key)) ? root.getStringArray(key) : [root.getString(key)];
|
|
2407
|
+
const values = [
|
|
2408
|
+
...new Set(
|
|
2409
|
+
rawValues.map((v) => v.split(/[ ,]/)).flat().filter(Boolean)
|
|
2410
|
+
)
|
|
2411
|
+
];
|
|
2412
|
+
if (!values.length) {
|
|
2413
|
+
return void 0;
|
|
2414
|
+
}
|
|
2415
|
+
if (validValues?.length) {
|
|
2416
|
+
for (const value of values) {
|
|
2417
|
+
if (!validValues.includes(value)) {
|
|
2418
|
+
const valid = validValues.map((k) => `'${k}'`).join(", ");
|
|
2419
|
+
throw new Error(
|
|
2420
|
+
`Invalid value '${value}' at '${key}' in 'permissionAttributes' config, valid values are ${valid}`
|
|
2421
|
+
);
|
|
2067
2422
|
}
|
|
2068
|
-
this.logger.info(`Signing key has expired, generating new key`);
|
|
2069
|
-
delete this.privateKeyPromise;
|
|
2070
2423
|
}
|
|
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;
|
|
2424
|
+
}
|
|
2425
|
+
return values;
|
|
2426
|
+
}
|
|
2427
|
+
function readPermissionNames(externalAccessEntryConfig) {
|
|
2428
|
+
return readStringOrStringArrayFromConfig(
|
|
2429
|
+
externalAccessEntryConfig,
|
|
2430
|
+
"permission"
|
|
2431
|
+
);
|
|
2432
|
+
}
|
|
2433
|
+
function readPermissionAttributes(externalAccessEntryConfig) {
|
|
2434
|
+
const config = externalAccessEntryConfig.getOptionalConfig(
|
|
2435
|
+
"permissionAttribute"
|
|
2436
|
+
);
|
|
2437
|
+
if (!config) {
|
|
2438
|
+
return void 0;
|
|
2439
|
+
}
|
|
2440
|
+
const validKeys = ["action"];
|
|
2441
|
+
for (const key of config.keys()) {
|
|
2442
|
+
if (!validKeys.includes(key)) {
|
|
2443
|
+
const valid = validKeys.map((k) => `'${k}'`).join(", ");
|
|
2444
|
+
throw new Error(
|
|
2445
|
+
`Invalid key '${key}' in 'permissionAttribute' config, expected ${valid}`
|
|
2446
|
+
);
|
|
2098
2447
|
}
|
|
2099
|
-
return promise;
|
|
2100
2448
|
}
|
|
2449
|
+
const action = readStringOrStringArrayFromConfig(config, "action", [
|
|
2450
|
+
"create",
|
|
2451
|
+
"read",
|
|
2452
|
+
"update",
|
|
2453
|
+
"delete"
|
|
2454
|
+
]);
|
|
2455
|
+
const result = {
|
|
2456
|
+
...action ? { action } : {}
|
|
2457
|
+
};
|
|
2458
|
+
return Object.keys(result).length ? result : void 0;
|
|
2101
2459
|
}
|
|
2102
2460
|
|
|
2103
|
-
class
|
|
2104
|
-
|
|
2105
|
-
|
|
2461
|
+
class LegacyTokenHandler {
|
|
2462
|
+
#entries = new Array();
|
|
2463
|
+
add(config) {
|
|
2464
|
+
const allAccessRestrictions = readAccessRestrictionsFromConfig(config);
|
|
2465
|
+
this.#doAdd(
|
|
2466
|
+
config.getString("options.secret"),
|
|
2467
|
+
config.getString("options.subject"),
|
|
2468
|
+
allAccessRestrictions
|
|
2469
|
+
);
|
|
2106
2470
|
}
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
return new URL(`${url}/.well-known/jwks.json`);
|
|
2111
|
-
});
|
|
2112
|
-
return new UserTokenHandler(jwksClient);
|
|
2471
|
+
// used only for the old backend.auth.keys array
|
|
2472
|
+
addOld(config) {
|
|
2473
|
+
this.#doAdd(config.getString("secret"), "external:backstage-plugin");
|
|
2113
2474
|
}
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2475
|
+
#doAdd(secret, subject, allAccessRestrictions) {
|
|
2476
|
+
if (!secret.match(/^\S+$/)) {
|
|
2477
|
+
throw new Error("Illegal secret, must be a valid base64 string");
|
|
2478
|
+
} else if (!subject.match(/^\S+$/)) {
|
|
2479
|
+
throw new Error("Illegal subject, must be a set of non-space characters");
|
|
2118
2480
|
}
|
|
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");
|
|
2481
|
+
let key;
|
|
2482
|
+
try {
|
|
2483
|
+
key = jose.base64url.decode(secret);
|
|
2484
|
+
} catch {
|
|
2485
|
+
throw new Error("Illegal secret, must be a valid base64 string");
|
|
2130
2486
|
}
|
|
2131
|
-
|
|
2487
|
+
if (this.#entries.some((e) => e.key === key)) {
|
|
2488
|
+
throw new Error(
|
|
2489
|
+
"Legacy externalAccess token was declared more than once"
|
|
2490
|
+
);
|
|
2491
|
+
}
|
|
2492
|
+
this.#entries.push({
|
|
2493
|
+
key,
|
|
2494
|
+
result: {
|
|
2495
|
+
subject,
|
|
2496
|
+
allAccessRestrictions
|
|
2497
|
+
}
|
|
2498
|
+
});
|
|
2132
2499
|
}
|
|
2133
|
-
|
|
2500
|
+
async verifyToken(token) {
|
|
2134
2501
|
try {
|
|
2135
|
-
const {
|
|
2136
|
-
if (
|
|
2137
|
-
return
|
|
2138
|
-
requiredClaims: ["iat", "exp", "sub"],
|
|
2139
|
-
typ: pluginAuthNode.tokenTypes.user.typParam
|
|
2140
|
-
};
|
|
2502
|
+
const { alg } = jose.decodeProtectedHeader(token);
|
|
2503
|
+
if (alg !== "HS256") {
|
|
2504
|
+
return void 0;
|
|
2141
2505
|
}
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
typ: pluginAuthNode.tokenTypes.limitedUser.typParam
|
|
2146
|
-
};
|
|
2506
|
+
const { sub, aud } = jose.decodeJwt(token);
|
|
2507
|
+
if (sub !== "backstage-server" || aud) {
|
|
2508
|
+
return void 0;
|
|
2147
2509
|
}
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2510
|
+
} catch (e) {
|
|
2511
|
+
return void 0;
|
|
2512
|
+
}
|
|
2513
|
+
for (const { key, result } of this.#entries) {
|
|
2514
|
+
try {
|
|
2515
|
+
await jose.jwtVerify(token, key);
|
|
2516
|
+
return result;
|
|
2517
|
+
} catch (e) {
|
|
2518
|
+
if (e.code !== "ERR_JWS_SIGNATURE_VERIFICATION_FAILED") {
|
|
2519
|
+
throw e;
|
|
2520
|
+
}
|
|
2153
2521
|
}
|
|
2154
|
-
} catch {
|
|
2155
2522
|
}
|
|
2156
2523
|
return void 0;
|
|
2157
2524
|
}
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
);
|
|
2166
|
-
const
|
|
2167
|
-
if (!
|
|
2168
|
-
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2527
|
+
const MIN_TOKEN_LENGTH = 8;
|
|
2528
|
+
class StaticTokenHandler {
|
|
2529
|
+
#entries = /* @__PURE__ */ new Map();
|
|
2530
|
+
add(config) {
|
|
2531
|
+
const token = config.getString("options.token");
|
|
2532
|
+
const subject = config.getString("options.subject");
|
|
2533
|
+
const allAccessRestrictions = readAccessRestrictionsFromConfig(config);
|
|
2534
|
+
if (!token.match(/^\S+$/)) {
|
|
2535
|
+
throw new Error("Illegal token, must be a set of non-space characters");
|
|
2536
|
+
} else if (token.length < MIN_TOKEN_LENGTH) {
|
|
2537
|
+
throw new Error(
|
|
2538
|
+
`Illegal token, must be at least ${MIN_TOKEN_LENGTH} characters length`
|
|
2539
|
+
);
|
|
2540
|
+
} else if (!subject.match(/^\S+$/)) {
|
|
2541
|
+
throw new Error("Illegal subject, must be a set of non-space characters");
|
|
2542
|
+
} else if (this.#entries.has(token)) {
|
|
2543
|
+
throw new Error(
|
|
2544
|
+
"Static externalAccess token was declared more than once"
|
|
2545
|
+
);
|
|
2169
2546
|
}
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2547
|
+
this.#entries.set(token, { subject, allAccessRestrictions });
|
|
2548
|
+
}
|
|
2549
|
+
async verifyToken(token) {
|
|
2550
|
+
return this.#entries.get(token);
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
class JWKSHandler {
|
|
2555
|
+
#entries = [];
|
|
2556
|
+
add(config) {
|
|
2557
|
+
if (!config.getString("options.url").match(/^\S+$/)) {
|
|
2558
|
+
throw new Error(
|
|
2559
|
+
"Illegal JWKS URL, must be a set of non-space characters"
|
|
2173
2560
|
);
|
|
2174
2561
|
}
|
|
2175
|
-
const
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2562
|
+
const algorithms = readStringOrStringArrayFromConfig(
|
|
2563
|
+
config,
|
|
2564
|
+
"options.algorithm"
|
|
2565
|
+
);
|
|
2566
|
+
const issuers = readStringOrStringArrayFromConfig(config, "options.issuer");
|
|
2567
|
+
const audiences = readStringOrStringArrayFromConfig(
|
|
2568
|
+
config,
|
|
2569
|
+
"options.audience"
|
|
2570
|
+
);
|
|
2571
|
+
const subjectPrefix = config.getOptionalString("options.subjectPrefix");
|
|
2572
|
+
const url = new URL(config.getString("options.url"));
|
|
2573
|
+
const jwks = jose.createRemoteJWKSet(url);
|
|
2574
|
+
const allAccessRestrictions = readAccessRestrictionsFromConfig(config);
|
|
2575
|
+
this.#entries.push({
|
|
2576
|
+
algorithms,
|
|
2577
|
+
audiences,
|
|
2578
|
+
issuers,
|
|
2579
|
+
jwks,
|
|
2580
|
+
subjectPrefix,
|
|
2581
|
+
url,
|
|
2582
|
+
allAccessRestrictions
|
|
2583
|
+
});
|
|
2194
2584
|
}
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2585
|
+
async verifyToken(token) {
|
|
2586
|
+
for (const entry of this.#entries) {
|
|
2587
|
+
try {
|
|
2588
|
+
const {
|
|
2589
|
+
payload: { sub }
|
|
2590
|
+
} = await jose.jwtVerify(token, entry.jwks, {
|
|
2591
|
+
algorithms: entry.algorithms,
|
|
2592
|
+
issuer: entry.issuers,
|
|
2593
|
+
audience: entry.audiences
|
|
2594
|
+
});
|
|
2595
|
+
if (sub) {
|
|
2596
|
+
const prefix = entry.subjectPrefix ? `external:${entry.subjectPrefix}:` : "external:";
|
|
2597
|
+
return {
|
|
2598
|
+
subject: `${prefix}${sub}`,
|
|
2599
|
+
allAccessRestrictions: entry.allAccessRestrictions
|
|
2600
|
+
};
|
|
2601
|
+
}
|
|
2602
|
+
} catch {
|
|
2603
|
+
continue;
|
|
2604
|
+
}
|
|
2201
2605
|
}
|
|
2606
|
+
return void 0;
|
|
2202
2607
|
}
|
|
2203
2608
|
}
|
|
2204
2609
|
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2610
|
+
const NEW_CONFIG_KEY = "backend.auth.externalAccess";
|
|
2611
|
+
const OLD_CONFIG_KEY = "backend.auth.keys";
|
|
2612
|
+
let loggedDeprecationWarning = false;
|
|
2613
|
+
class ExternalTokenHandler {
|
|
2614
|
+
constructor(ownPluginId, handlers) {
|
|
2615
|
+
this.ownPluginId = ownPluginId;
|
|
2616
|
+
this.handlers = handlers;
|
|
2617
|
+
}
|
|
2618
|
+
static create(options) {
|
|
2619
|
+
const { ownPluginId, config, logger } = options;
|
|
2620
|
+
const staticHandler = new StaticTokenHandler();
|
|
2621
|
+
const legacyHandler = new LegacyTokenHandler();
|
|
2622
|
+
const jwksHandler = new JWKSHandler();
|
|
2623
|
+
const handlers = {
|
|
2624
|
+
static: staticHandler,
|
|
2625
|
+
legacy: legacyHandler,
|
|
2626
|
+
jwks: jwksHandler
|
|
2627
|
+
};
|
|
2628
|
+
const handlerConfigs = config.getOptionalConfigArray(NEW_CONFIG_KEY) ?? [];
|
|
2629
|
+
for (const handlerConfig of handlerConfigs) {
|
|
2630
|
+
const type = handlerConfig.getString("type");
|
|
2631
|
+
const handler = handlers[type];
|
|
2632
|
+
if (!handler) {
|
|
2633
|
+
const valid = Object.keys(handlers).map((k) => `'${k}'`).join(", ");
|
|
2213
2634
|
throw new Error(
|
|
2214
|
-
`
|
|
2635
|
+
`Unknown type '${type}' in ${NEW_CONFIG_KEY}, expected one of ${valid}`
|
|
2215
2636
|
);
|
|
2216
2637
|
}
|
|
2638
|
+
handler.add(handlerConfig);
|
|
2217
2639
|
}
|
|
2218
|
-
const
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
`Attempted to declare 'accessRestrictions' twice for plugin '${pluginId}', which is not permitted`
|
|
2640
|
+
const legacyConfigs = config.getOptionalConfigArray(OLD_CONFIG_KEY) ?? [];
|
|
2641
|
+
if (legacyConfigs.length && !loggedDeprecationWarning) {
|
|
2642
|
+
loggedDeprecationWarning = true;
|
|
2643
|
+
logger.warn(
|
|
2644
|
+
`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
2645
|
);
|
|
2225
2646
|
}
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2647
|
+
for (const handlerConfig of legacyConfigs) {
|
|
2648
|
+
legacyHandler.addOld(handlerConfig);
|
|
2649
|
+
}
|
|
2650
|
+
return new ExternalTokenHandler(ownPluginId, Object.values(handlers));
|
|
2651
|
+
}
|
|
2652
|
+
async verifyToken(token) {
|
|
2653
|
+
for (const handler of this.handlers) {
|
|
2654
|
+
const result = await handler.verifyToken(token);
|
|
2655
|
+
if (result) {
|
|
2656
|
+
const { allAccessRestrictions, ...rest } = result;
|
|
2657
|
+
if (allAccessRestrictions) {
|
|
2658
|
+
const accessRestrictions = allAccessRestrictions.get(
|
|
2659
|
+
this.ownPluginId
|
|
2660
|
+
);
|
|
2661
|
+
if (!accessRestrictions) {
|
|
2662
|
+
const valid = [...allAccessRestrictions.keys()].map((k) => `'${k}'`).join(", ");
|
|
2663
|
+
throw new errors.NotAllowedError(
|
|
2664
|
+
`This token's access is restricted to plugin(s) ${valid}`
|
|
2665
|
+
);
|
|
2666
|
+
}
|
|
2667
|
+
return {
|
|
2668
|
+
...rest,
|
|
2669
|
+
accessRestrictions
|
|
2670
|
+
};
|
|
2671
|
+
}
|
|
2672
|
+
return rest;
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
return void 0;
|
|
2230
2676
|
}
|
|
2231
|
-
return result.size ? result : void 0;
|
|
2232
2677
|
}
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2678
|
+
|
|
2679
|
+
const CLOCK_MARGIN_S = 10;
|
|
2680
|
+
class JwksClient {
|
|
2681
|
+
constructor(getEndpoint) {
|
|
2682
|
+
this.getEndpoint = getEndpoint;
|
|
2236
2683
|
}
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2684
|
+
#keyStore;
|
|
2685
|
+
#keyStoreUpdated = 0;
|
|
2686
|
+
get getKey() {
|
|
2687
|
+
if (!this.#keyStore) {
|
|
2688
|
+
throw new errors.AuthenticationError(
|
|
2689
|
+
"refreshKeyStore must be called before jwksClient.getKey"
|
|
2690
|
+
);
|
|
2691
|
+
}
|
|
2692
|
+
return this.#keyStore;
|
|
2245
2693
|
}
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2694
|
+
/**
|
|
2695
|
+
* If the last keystore refresh is stale, update the keystore URL to the latest
|
|
2696
|
+
*/
|
|
2697
|
+
async refreshKeyStore(rawJwtToken) {
|
|
2698
|
+
const payload = await jose.decodeJwt(rawJwtToken);
|
|
2699
|
+
const header = await jose.decodeProtectedHeader(rawJwtToken);
|
|
2700
|
+
let keyStoreHasKey;
|
|
2701
|
+
try {
|
|
2702
|
+
if (this.#keyStore) {
|
|
2703
|
+
const [_, rawPayload, rawSignature] = rawJwtToken.split(".");
|
|
2704
|
+
keyStoreHasKey = await this.#keyStore(header, {
|
|
2705
|
+
payload: rawPayload,
|
|
2706
|
+
signature: rawSignature
|
|
2707
|
+
});
|
|
2253
2708
|
}
|
|
2709
|
+
} catch (error) {
|
|
2710
|
+
keyStoreHasKey = false;
|
|
2254
2711
|
}
|
|
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
|
-
);
|
|
2712
|
+
const issuedAfterLastRefresh = payload?.iat && payload.iat > this.#keyStoreUpdated - CLOCK_MARGIN_S;
|
|
2713
|
+
if (!this.#keyStore || !keyStoreHasKey && issuedAfterLastRefresh) {
|
|
2714
|
+
const endpoint = await this.getEndpoint();
|
|
2715
|
+
this.#keyStore = jose.createRemoteJWKSet(endpoint);
|
|
2716
|
+
this.#keyStoreUpdated = Date.now() / 1e3;
|
|
2278
2717
|
}
|
|
2279
2718
|
}
|
|
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
2719
|
}
|
|
2291
2720
|
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
this
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2721
|
+
const SECONDS_IN_MS$2 = 1e3;
|
|
2722
|
+
const ALLOWED_PLUGIN_ID_PATTERN = /^[a-z0-9_-]+$/i;
|
|
2723
|
+
class PluginTokenHandler {
|
|
2724
|
+
constructor(logger, ownPluginId, keySource, algorithm, keyDurationSeconds, discovery) {
|
|
2725
|
+
this.logger = logger;
|
|
2726
|
+
this.ownPluginId = ownPluginId;
|
|
2727
|
+
this.keySource = keySource;
|
|
2728
|
+
this.algorithm = algorithm;
|
|
2729
|
+
this.keyDurationSeconds = keyDurationSeconds;
|
|
2730
|
+
this.discovery = discovery;
|
|
2301
2731
|
}
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2732
|
+
jwksMap = /* @__PURE__ */ new Map();
|
|
2733
|
+
// Tracking state for isTargetPluginSupported
|
|
2734
|
+
supportedTargetPlugins = /* @__PURE__ */ new Set();
|
|
2735
|
+
targetPluginInflightChecks = /* @__PURE__ */ new Map();
|
|
2736
|
+
static create(options) {
|
|
2737
|
+
return new PluginTokenHandler(
|
|
2738
|
+
options.logger,
|
|
2739
|
+
options.ownPluginId,
|
|
2740
|
+
options.keySource,
|
|
2741
|
+
options.algorithm ?? "ES256",
|
|
2742
|
+
Math.round(types.durationToMilliseconds(options.keyDuration) / 1e3),
|
|
2743
|
+
options.discovery
|
|
2744
|
+
);
|
|
2305
2745
|
}
|
|
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;
|
|
2746
|
+
async verifyToken(token) {
|
|
2313
2747
|
try {
|
|
2314
|
-
|
|
2748
|
+
const { typ } = jose.decodeProtectedHeader(token);
|
|
2749
|
+
if (typ !== pluginAuthNode.tokenTypes.plugin.typParam) {
|
|
2750
|
+
return void 0;
|
|
2751
|
+
}
|
|
2315
2752
|
} catch {
|
|
2316
|
-
|
|
2753
|
+
return void 0;
|
|
2317
2754
|
}
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2755
|
+
const pluginId = String(jose.decodeJwt(token).sub);
|
|
2756
|
+
if (!pluginId) {
|
|
2757
|
+
throw new errors.AuthenticationError("Invalid plugin token: missing subject");
|
|
2758
|
+
}
|
|
2759
|
+
if (!ALLOWED_PLUGIN_ID_PATTERN.test(pluginId)) {
|
|
2760
|
+
throw new errors.AuthenticationError(
|
|
2761
|
+
"Invalid plugin token: forbidden subject format"
|
|
2321
2762
|
);
|
|
2322
2763
|
}
|
|
2323
|
-
this
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2764
|
+
const jwksClient = await this.getJwksClient(pluginId);
|
|
2765
|
+
await jwksClient.refreshKeyStore(token);
|
|
2766
|
+
const { payload } = await jose.jwtVerify(
|
|
2767
|
+
token,
|
|
2768
|
+
jwksClient.getKey,
|
|
2769
|
+
{
|
|
2770
|
+
typ: pluginAuthNode.tokenTypes.plugin.typParam,
|
|
2771
|
+
audience: this.ownPluginId,
|
|
2772
|
+
requiredClaims: ["iat", "exp", "sub", "aud"]
|
|
2328
2773
|
}
|
|
2774
|
+
).catch((e) => {
|
|
2775
|
+
throw new errors.AuthenticationError("Invalid plugin token", e);
|
|
2329
2776
|
});
|
|
2777
|
+
return { subject: `plugin:${payload.sub}`, limitedUserToken: payload.obo };
|
|
2330
2778
|
}
|
|
2331
|
-
async
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2779
|
+
async issueToken(options) {
|
|
2780
|
+
const { pluginId, targetPluginId, onBehalfOf } = options;
|
|
2781
|
+
const key = await this.keySource.getPrivateSigningKey();
|
|
2782
|
+
const sub = pluginId;
|
|
2783
|
+
const aud = targetPluginId;
|
|
2784
|
+
const iat = Math.floor(Date.now() / SECONDS_IN_MS$2);
|
|
2785
|
+
const ourExp = iat + this.keyDurationSeconds;
|
|
2786
|
+
const exp = onBehalfOf ? Math.min(
|
|
2787
|
+
ourExp,
|
|
2788
|
+
Math.floor(onBehalfOf.expiresAt.getTime() / SECONDS_IN_MS$2)
|
|
2789
|
+
) : ourExp;
|
|
2790
|
+
const claims = { sub, aud, iat, exp, obo: onBehalfOf?.token };
|
|
2791
|
+
const token = await new jose.SignJWT(claims).setProtectedHeader({
|
|
2792
|
+
typ: pluginAuthNode.tokenTypes.plugin.typParam,
|
|
2793
|
+
alg: this.algorithm,
|
|
2794
|
+
kid: key.kid
|
|
2795
|
+
}).setAudience(aud).setSubject(sub).setIssuedAt(iat).setExpirationTime(exp).sign(await jose.importJWK(key));
|
|
2796
|
+
return { token };
|
|
2797
|
+
}
|
|
2798
|
+
async isTargetPluginSupported(targetPluginId) {
|
|
2799
|
+
if (this.supportedTargetPlugins.has(targetPluginId)) {
|
|
2800
|
+
return true;
|
|
2343
2801
|
}
|
|
2344
|
-
|
|
2802
|
+
const inFlight = this.targetPluginInflightChecks.get(targetPluginId);
|
|
2803
|
+
if (inFlight) {
|
|
2804
|
+
return inFlight;
|
|
2805
|
+
}
|
|
2806
|
+
const doCheck = async () => {
|
|
2345
2807
|
try {
|
|
2346
|
-
await
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2808
|
+
const res = await fetch(
|
|
2809
|
+
`${await this.discovery.getBaseUrl(
|
|
2810
|
+
targetPluginId
|
|
2811
|
+
)}/.backstage/auth/v1/jwks.json`
|
|
2812
|
+
);
|
|
2813
|
+
if (res.status === 404) {
|
|
2814
|
+
return false;
|
|
2815
|
+
}
|
|
2816
|
+
if (!res.ok) {
|
|
2817
|
+
throw new Error(`Failed to fetch jwks.json, ${res.status}`);
|
|
2818
|
+
}
|
|
2819
|
+
const data = await res.json();
|
|
2820
|
+
if (!data.keys) {
|
|
2821
|
+
throw new Error(`Invalid jwks.json response, missing keys`);
|
|
2351
2822
|
}
|
|
2823
|
+
this.supportedTargetPlugins.add(targetPluginId);
|
|
2824
|
+
return true;
|
|
2825
|
+
} catch (error) {
|
|
2826
|
+
this.logger.error("Unexpected failure for target JWKS check", error);
|
|
2827
|
+
return false;
|
|
2828
|
+
} finally {
|
|
2829
|
+
this.targetPluginInflightChecks.delete(targetPluginId);
|
|
2352
2830
|
}
|
|
2353
|
-
}
|
|
2354
|
-
|
|
2831
|
+
};
|
|
2832
|
+
const check = doCheck();
|
|
2833
|
+
this.targetPluginInflightChecks.set(targetPluginId, check);
|
|
2834
|
+
return check;
|
|
2355
2835
|
}
|
|
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"
|
|
2836
|
+
async getJwksClient(pluginId) {
|
|
2837
|
+
const client = this.jwksMap.get(pluginId);
|
|
2838
|
+
if (client) {
|
|
2839
|
+
return client;
|
|
2840
|
+
}
|
|
2841
|
+
if (!await this.isTargetPluginSupported(pluginId)) {
|
|
2842
|
+
throw new errors.AuthenticationError(
|
|
2843
|
+
`Received a plugin token where the source '${pluginId}' plugin unexpectedly does not have a JWKS endpoint`
|
|
2376
2844
|
);
|
|
2377
2845
|
}
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2846
|
+
const newClient = new JwksClient(async () => {
|
|
2847
|
+
return new URL(
|
|
2848
|
+
`${await this.discovery.getBaseUrl(
|
|
2849
|
+
pluginId
|
|
2850
|
+
)}/.backstage/auth/v1/jwks.json`
|
|
2851
|
+
);
|
|
2852
|
+
});
|
|
2853
|
+
this.jwksMap.set(pluginId, newClient);
|
|
2854
|
+
return newClient;
|
|
2382
2855
|
}
|
|
2383
2856
|
}
|
|
2384
2857
|
|
|
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
|
-
});
|
|
2858
|
+
const MIGRATIONS_TABLE = "backstage_backend_public_keys__knex_migrations";
|
|
2859
|
+
const TABLE = "backstage_backend_public_keys__keys";
|
|
2860
|
+
function applyDatabaseMigrations(knex) {
|
|
2861
|
+
const migrationsDir = backendPluginApi.resolvePackagePath(
|
|
2862
|
+
"@backstage/backend-defaults",
|
|
2863
|
+
"migrations/auth"
|
|
2864
|
+
);
|
|
2865
|
+
return knex.migrate.latest({
|
|
2866
|
+
directory: migrationsDir,
|
|
2867
|
+
tableName: MIGRATIONS_TABLE
|
|
2868
|
+
});
|
|
2869
|
+
}
|
|
2870
|
+
class DatabaseKeyStore {
|
|
2871
|
+
constructor(client, logger) {
|
|
2872
|
+
this.client = client;
|
|
2873
|
+
this.logger = logger;
|
|
2415
2874
|
}
|
|
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
|
-
}
|
|
2875
|
+
static async create(options) {
|
|
2876
|
+
const { database, logger } = options;
|
|
2877
|
+
const client = await database.getClient();
|
|
2878
|
+
if (!database.migrations?.skip) {
|
|
2879
|
+
await applyDatabaseMigrations(client);
|
|
2436
2880
|
}
|
|
2437
|
-
return
|
|
2881
|
+
return new DatabaseKeyStore(client, logger);
|
|
2438
2882
|
}
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
constructor(ownPluginId, handlers) {
|
|
2446
|
-
this.ownPluginId = ownPluginId;
|
|
2447
|
-
this.handlers = handlers;
|
|
2883
|
+
async addKey(options) {
|
|
2884
|
+
await this.client(TABLE).insert({
|
|
2885
|
+
id: options.key.kid,
|
|
2886
|
+
key: JSON.stringify(options.key),
|
|
2887
|
+
expires_at: options.expiresAt.toISOString()
|
|
2888
|
+
});
|
|
2448
2889
|
}
|
|
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
|
-
);
|
|
2890
|
+
async listKeys() {
|
|
2891
|
+
const rows = await this.client(TABLE).select();
|
|
2892
|
+
const keys = rows.map((row) => ({
|
|
2893
|
+
id: row.id,
|
|
2894
|
+
key: JSON.parse(row.key),
|
|
2895
|
+
expiresAt: new Date(row.expires_at)
|
|
2896
|
+
}));
|
|
2897
|
+
const validKeys = [];
|
|
2898
|
+
const expiredKeys = [];
|
|
2899
|
+
for (const key of keys) {
|
|
2900
|
+
if (luxon.DateTime.fromJSDate(key.expiresAt) < luxon.DateTime.local()) {
|
|
2901
|
+
expiredKeys.push(key);
|
|
2902
|
+
} else {
|
|
2903
|
+
validKeys.push(key);
|
|
2468
2904
|
}
|
|
2469
|
-
handler.add(handlerConfig);
|
|
2470
2905
|
}
|
|
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`
|
|
2906
|
+
if (expiredKeys.length > 0) {
|
|
2907
|
+
const kids = expiredKeys.map(({ key }) => key.kid);
|
|
2908
|
+
this.logger.info(
|
|
2909
|
+
`Removing expired plugin service keys, '${kids.join("', '")}'`
|
|
2476
2910
|
);
|
|
2911
|
+
this.client(TABLE).delete().whereIn("id", kids).catch((error) => {
|
|
2912
|
+
this.logger.error(
|
|
2913
|
+
"Failed to remove expired plugin service keys",
|
|
2914
|
+
error
|
|
2915
|
+
);
|
|
2916
|
+
});
|
|
2477
2917
|
}
|
|
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;
|
|
2918
|
+
return { keys: validKeys };
|
|
2507
2919
|
}
|
|
2508
2920
|
}
|
|
2509
2921
|
|
|
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
|
|
2922
|
+
const SECONDS_IN_MS$1 = 1e3;
|
|
2923
|
+
const KEY_EXPIRATION_MARGIN_FACTOR = 3;
|
|
2924
|
+
class DatabasePluginKeySource {
|
|
2925
|
+
constructor(keyStore, logger, keyDurationSeconds, algorithm) {
|
|
2926
|
+
this.keyStore = keyStore;
|
|
2927
|
+
this.logger = logger;
|
|
2928
|
+
this.keyDurationSeconds = keyDurationSeconds;
|
|
2929
|
+
this.algorithm = algorithm;
|
|
2930
|
+
}
|
|
2931
|
+
privateKeyPromise;
|
|
2932
|
+
keyExpiry;
|
|
2933
|
+
static async create(options) {
|
|
2934
|
+
const keyStore = await DatabaseKeyStore.create({
|
|
2935
|
+
database: options.database,
|
|
2936
|
+
logger: options.logger
|
|
2548
2937
|
});
|
|
2549
|
-
return new
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2938
|
+
return new DatabasePluginKeySource(
|
|
2939
|
+
keyStore,
|
|
2940
|
+
options.logger,
|
|
2941
|
+
Math.round(types.durationToMilliseconds(options.keyDuration) / 1e3),
|
|
2942
|
+
options.algorithm ?? "ES256"
|
|
2943
|
+
);
|
|
2944
|
+
}
|
|
2945
|
+
async getPrivateSigningKey() {
|
|
2946
|
+
if (this.privateKeyPromise) {
|
|
2947
|
+
if (this.keyExpiry && this.keyExpiry.getTime() > Date.now()) {
|
|
2948
|
+
return this.privateKeyPromise;
|
|
2949
|
+
}
|
|
2950
|
+
this.logger.info(`Signing key has expired, generating new key`);
|
|
2951
|
+
delete this.privateKeyPromise;
|
|
2952
|
+
}
|
|
2953
|
+
this.keyExpiry = new Date(
|
|
2954
|
+
Date.now() + this.keyDurationSeconds * SECONDS_IN_MS$1
|
|
2557
2955
|
);
|
|
2956
|
+
const promise = (async () => {
|
|
2957
|
+
const kid = uuid.v4();
|
|
2958
|
+
const key = await jose.generateKeyPair(this.algorithm);
|
|
2959
|
+
const publicKey = await jose.exportJWK(key.publicKey);
|
|
2960
|
+
const privateKey = await jose.exportJWK(key.privateKey);
|
|
2961
|
+
publicKey.kid = privateKey.kid = kid;
|
|
2962
|
+
publicKey.alg = privateKey.alg = this.algorithm;
|
|
2963
|
+
this.logger.info(`Created new signing key ${kid}`);
|
|
2964
|
+
await this.keyStore.addKey({
|
|
2965
|
+
id: kid,
|
|
2966
|
+
key: publicKey,
|
|
2967
|
+
expiresAt: new Date(
|
|
2968
|
+
Date.now() + this.keyDurationSeconds * SECONDS_IN_MS$1 * KEY_EXPIRATION_MARGIN_FACTOR
|
|
2969
|
+
)
|
|
2970
|
+
});
|
|
2971
|
+
return privateKey;
|
|
2972
|
+
})();
|
|
2973
|
+
this.privateKeyPromise = promise;
|
|
2974
|
+
try {
|
|
2975
|
+
await promise;
|
|
2976
|
+
} catch (error) {
|
|
2977
|
+
this.logger.error(`Failed to generate new signing key, ${error}`);
|
|
2978
|
+
delete this.keyExpiry;
|
|
2979
|
+
delete this.privateKeyPromise;
|
|
2980
|
+
}
|
|
2981
|
+
return promise;
|
|
2558
2982
|
}
|
|
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();
|
|
2983
|
+
listKeys() {
|
|
2984
|
+
return this.keyStore.listKeys();
|
|
2573
2985
|
}
|
|
2574
|
-
}
|
|
2986
|
+
}
|
|
2575
2987
|
|
|
2576
|
-
const
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2988
|
+
const DEFAULT_ALGORITHM = "ES256";
|
|
2989
|
+
const SECONDS_IN_MS = 1e3;
|
|
2990
|
+
class StaticConfigPluginKeySource {
|
|
2991
|
+
constructor(keyPairs, keyDurationSeconds) {
|
|
2992
|
+
this.keyPairs = keyPairs;
|
|
2993
|
+
this.keyDurationSeconds = keyDurationSeconds;
|
|
2994
|
+
}
|
|
2995
|
+
static async create(options) {
|
|
2996
|
+
const keyConfigs = options.sourceConfig.getConfigArray("static.keys").map((c) => {
|
|
2997
|
+
const staticKeyConfig = {
|
|
2998
|
+
publicKeyFile: c.getString("publicKeyFile"),
|
|
2999
|
+
privateKeyFile: c.getOptionalString("privateKeyFile"),
|
|
3000
|
+
keyId: c.getString("keyId"),
|
|
3001
|
+
algorithm: c.getOptionalString("algorithm") ?? DEFAULT_ALGORITHM
|
|
3002
|
+
};
|
|
3003
|
+
return staticKeyConfig;
|
|
3004
|
+
});
|
|
3005
|
+
const keyPairs = await Promise.all(
|
|
3006
|
+
keyConfigs.map(async (k) => await this.loadKeyPair(k))
|
|
3007
|
+
);
|
|
3008
|
+
if (keyPairs.length < 1) {
|
|
3009
|
+
throw new Error(
|
|
3010
|
+
"At least one key pair must be provided in static.keys, when the static key store type is used"
|
|
3011
|
+
);
|
|
3012
|
+
} else if (!keyPairs[0].privateKey) {
|
|
3013
|
+
throw new Error(
|
|
3014
|
+
"Private key for signing must be provided in the first key pair in static.keys, when the static key store type is used"
|
|
3015
|
+
);
|
|
2588
3016
|
}
|
|
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
|
-
})
|
|
3017
|
+
return new StaticConfigPluginKeySource(
|
|
3018
|
+
keyPairs,
|
|
3019
|
+
types.durationToMilliseconds(options.keyDuration) / SECONDS_IN_MS
|
|
2606
3020
|
);
|
|
2607
|
-
}
|
|
2608
|
-
async
|
|
2609
|
-
return
|
|
2610
|
-
|
|
2611
|
-
|
|
3021
|
+
}
|
|
3022
|
+
async getPrivateSigningKey() {
|
|
3023
|
+
return this.keyPairs[0].privateKey;
|
|
3024
|
+
}
|
|
3025
|
+
async listKeys() {
|
|
3026
|
+
const keys = this.keyPairs.map((k) => this.keyPairToStoredKey(k));
|
|
3027
|
+
return { keys };
|
|
3028
|
+
}
|
|
3029
|
+
static async loadKeyPair(options) {
|
|
3030
|
+
const algorithm = options.algorithm;
|
|
3031
|
+
const keyId = options.keyId;
|
|
3032
|
+
const publicKey = await this.loadPublicKeyFromFile(
|
|
3033
|
+
options.publicKeyFile,
|
|
3034
|
+
keyId,
|
|
3035
|
+
algorithm
|
|
3036
|
+
);
|
|
3037
|
+
const privateKey = options.privateKeyFile ? await this.loadPrivateKeyFromFile(
|
|
3038
|
+
options.privateKeyFile,
|
|
3039
|
+
keyId,
|
|
3040
|
+
algorithm
|
|
3041
|
+
) : void 0;
|
|
3042
|
+
return { publicKey, privateKey, keyId };
|
|
3043
|
+
}
|
|
3044
|
+
static async loadPublicKeyFromFile(path, keyId, algorithm) {
|
|
3045
|
+
return this.loadKeyFromFile(path, keyId, algorithm, jose.importSPKI);
|
|
3046
|
+
}
|
|
3047
|
+
static async loadPrivateKeyFromFile(path, keyId, algorithm) {
|
|
3048
|
+
return this.loadKeyFromFile(path, keyId, algorithm, jose.importPKCS8);
|
|
3049
|
+
}
|
|
3050
|
+
static async loadKeyFromFile(path, keyId, algorithm, importer) {
|
|
3051
|
+
const content = await fs$1.promises.readFile(path, { encoding: "utf8", flag: "r" });
|
|
3052
|
+
const key = await importer(content, algorithm);
|
|
3053
|
+
const jwk = await jose.exportJWK(key);
|
|
3054
|
+
jwk.kid = keyId;
|
|
3055
|
+
jwk.alg = algorithm;
|
|
3056
|
+
return jwk;
|
|
3057
|
+
}
|
|
3058
|
+
keyPairToStoredKey(keyPair) {
|
|
3059
|
+
const publicKey = {
|
|
3060
|
+
...keyPair.publicKey,
|
|
3061
|
+
kid: keyPair.keyId
|
|
3062
|
+
};
|
|
3063
|
+
return {
|
|
3064
|
+
key: publicKey,
|
|
3065
|
+
id: keyPair.keyId,
|
|
3066
|
+
expiresAt: new Date(Date.now() + this.keyDurationSeconds * SECONDS_IN_MS)
|
|
3067
|
+
};
|
|
3068
|
+
}
|
|
3069
|
+
}
|
|
3070
|
+
|
|
3071
|
+
const CONFIG_ROOT_KEY = "backend.auth.pluginKeyStore";
|
|
3072
|
+
async function createPluginKeySource(options) {
|
|
3073
|
+
const keyStoreConfig = options.config.getOptionalConfig(CONFIG_ROOT_KEY);
|
|
3074
|
+
const type = keyStoreConfig?.getOptionalString("type") ?? "database";
|
|
3075
|
+
if (!keyStoreConfig || type === "database") {
|
|
3076
|
+
return DatabasePluginKeySource.create({
|
|
3077
|
+
database: options.database,
|
|
3078
|
+
logger: options.logger,
|
|
3079
|
+
keyDuration: options.keyDuration,
|
|
3080
|
+
algorithm: options.algorithm
|
|
3081
|
+
});
|
|
3082
|
+
} else if (type === "static") {
|
|
3083
|
+
return StaticConfigPluginKeySource.create({
|
|
3084
|
+
sourceConfig: keyStoreConfig,
|
|
3085
|
+
keyDuration: options.keyDuration
|
|
2612
3086
|
});
|
|
2613
3087
|
}
|
|
2614
|
-
|
|
3088
|
+
throw new Error(
|
|
3089
|
+
`Unsupported config value ${CONFIG_ROOT_KEY}.type '${type}'; expected one of 'database', 'static'`
|
|
3090
|
+
);
|
|
3091
|
+
}
|
|
2615
3092
|
|
|
2616
|
-
class
|
|
2617
|
-
constructor(
|
|
2618
|
-
this.
|
|
2619
|
-
this.externalBaseUrl = externalBaseUrl;
|
|
2620
|
-
this.discoveryConfig = discoveryConfig;
|
|
3093
|
+
class UserTokenHandler {
|
|
3094
|
+
constructor(jwksClient) {
|
|
3095
|
+
this.jwksClient = jwksClient;
|
|
2621
3096
|
}
|
|
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";
|
|
3097
|
+
static create(options) {
|
|
3098
|
+
const jwksClient = new JwksClient(async () => {
|
|
3099
|
+
const url = await options.discovery.getBaseUrl("auth");
|
|
3100
|
+
return new URL(`${url}/.well-known/jwks.json`);
|
|
3101
|
+
});
|
|
3102
|
+
return new UserTokenHandler(jwksClient);
|
|
3103
|
+
}
|
|
3104
|
+
async verifyToken(token) {
|
|
3105
|
+
const verifyOpts = this.#getTokenVerificationOptions(token);
|
|
3106
|
+
if (!verifyOpts) {
|
|
3107
|
+
return void 0;
|
|
2658
3108
|
}
|
|
2659
|
-
|
|
2660
|
-
|
|
3109
|
+
await this.jwksClient.refreshKeyStore(token);
|
|
3110
|
+
const { payload } = await jose.jwtVerify(
|
|
3111
|
+
token,
|
|
3112
|
+
this.jwksClient.getKey,
|
|
3113
|
+
verifyOpts
|
|
3114
|
+
).catch((e) => {
|
|
3115
|
+
throw new errors.AuthenticationError("Invalid token", e);
|
|
3116
|
+
});
|
|
3117
|
+
const userEntityRef = payload.sub;
|
|
3118
|
+
if (!userEntityRef) {
|
|
3119
|
+
throw new errors.AuthenticationError("No user sub found in token");
|
|
2661
3120
|
}
|
|
2662
|
-
|
|
2663
|
-
return new HostDiscovery(
|
|
2664
|
-
internalBaseUrl + basePath,
|
|
2665
|
-
externalBaseUrl + basePath,
|
|
2666
|
-
config.getOptionalConfig("discovery")
|
|
2667
|
-
);
|
|
3121
|
+
return { userEntityRef };
|
|
2668
3122
|
}
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
3123
|
+
#getTokenVerificationOptions(token) {
|
|
3124
|
+
try {
|
|
3125
|
+
const { typ } = jose.decodeProtectedHeader(token);
|
|
3126
|
+
if (typ === pluginAuthNode.tokenTypes.user.typParam) {
|
|
3127
|
+
return {
|
|
3128
|
+
requiredClaims: ["iat", "exp", "sub"],
|
|
3129
|
+
typ: pluginAuthNode.tokenTypes.user.typParam
|
|
3130
|
+
};
|
|
3131
|
+
}
|
|
3132
|
+
if (typ === pluginAuthNode.tokenTypes.limitedUser.typParam) {
|
|
3133
|
+
return {
|
|
3134
|
+
requiredClaims: ["iat", "exp", "sub"],
|
|
3135
|
+
typ: pluginAuthNode.tokenTypes.limitedUser.typParam
|
|
3136
|
+
};
|
|
3137
|
+
}
|
|
3138
|
+
const { aud } = jose.decodeJwt(token);
|
|
3139
|
+
if (aud === pluginAuthNode.tokenTypes.user.audClaim) {
|
|
3140
|
+
return {
|
|
3141
|
+
audience: pluginAuthNode.tokenTypes.user.audClaim
|
|
3142
|
+
};
|
|
3143
|
+
}
|
|
3144
|
+
} catch {
|
|
2675
3145
|
}
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
3146
|
+
return void 0;
|
|
3147
|
+
}
|
|
3148
|
+
createLimitedUserToken(backstageToken) {
|
|
3149
|
+
const [headerRaw, payloadRaw] = backstageToken.split(".");
|
|
3150
|
+
const header = JSON.parse(
|
|
3151
|
+
new TextDecoder().decode(jose.base64url.decode(headerRaw))
|
|
3152
|
+
);
|
|
3153
|
+
const payload = JSON.parse(
|
|
3154
|
+
new TextDecoder().decode(jose.base64url.decode(payloadRaw))
|
|
3155
|
+
);
|
|
3156
|
+
const tokenType = header.typ;
|
|
3157
|
+
if (!tokenType || tokenType === pluginAuthNode.tokenTypes.limitedUser.typParam) {
|
|
3158
|
+
return { token: backstageToken, expiresAt: new Date(payload.exp * 1e3) };
|
|
3159
|
+
}
|
|
3160
|
+
if (tokenType !== pluginAuthNode.tokenTypes.user.typParam) {
|
|
3161
|
+
throw new errors.AuthenticationError(
|
|
3162
|
+
"Failed to create limited user token, invalid token type"
|
|
2680
3163
|
);
|
|
2681
3164
|
}
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
3165
|
+
const limitedUserToken = [
|
|
3166
|
+
jose.base64url.encode(
|
|
3167
|
+
JSON.stringify({
|
|
3168
|
+
typ: pluginAuthNode.tokenTypes.limitedUser.typParam,
|
|
3169
|
+
alg: header.alg,
|
|
3170
|
+
kid: header.kid
|
|
3171
|
+
})
|
|
3172
|
+
),
|
|
3173
|
+
jose.base64url.encode(
|
|
3174
|
+
JSON.stringify({
|
|
3175
|
+
sub: payload.sub,
|
|
3176
|
+
iat: payload.iat,
|
|
3177
|
+
exp: payload.exp
|
|
3178
|
+
})
|
|
3179
|
+
),
|
|
3180
|
+
payload.uip
|
|
3181
|
+
].join(".");
|
|
3182
|
+
return { token: limitedUserToken, expiresAt: new Date(payload.exp * 1e3) };
|
|
2689
3183
|
}
|
|
2690
|
-
|
|
2691
|
-
|
|
3184
|
+
isLimitedUserToken(token) {
|
|
3185
|
+
try {
|
|
3186
|
+
const { typ } = jose.decodeProtectedHeader(token);
|
|
3187
|
+
return typ === pluginAuthNode.tokenTypes.limitedUser.typParam;
|
|
3188
|
+
} catch {
|
|
3189
|
+
return false;
|
|
3190
|
+
}
|
|
2692
3191
|
}
|
|
2693
3192
|
}
|
|
2694
3193
|
|
|
2695
|
-
const
|
|
2696
|
-
service: backendPluginApi.coreServices.
|
|
3194
|
+
const authServiceFactory$1 = backendPluginApi.createServiceFactory({
|
|
3195
|
+
service: backendPluginApi.coreServices.auth,
|
|
2697
3196
|
deps: {
|
|
2698
|
-
config: backendPluginApi.coreServices.rootConfig
|
|
3197
|
+
config: backendPluginApi.coreServices.rootConfig,
|
|
3198
|
+
logger: backendPluginApi.coreServices.rootLogger,
|
|
3199
|
+
discovery: backendPluginApi.coreServices.discovery,
|
|
3200
|
+
plugin: backendPluginApi.coreServices.pluginMetadata,
|
|
3201
|
+
database: backendPluginApi.coreServices.database,
|
|
3202
|
+
// Re-using the token manager makes sure that we use the same generated keys for
|
|
3203
|
+
// development as plugins that have not yet been migrated. It's important that this
|
|
3204
|
+
// keeps working as long as there are plugins that have not been migrated to the
|
|
3205
|
+
// new auth services in the new backend system.
|
|
3206
|
+
tokenManager: backendPluginApi.coreServices.tokenManager
|
|
2699
3207
|
},
|
|
2700
|
-
async factory({ config }) {
|
|
2701
|
-
|
|
3208
|
+
async factory({ config, discovery, plugin, tokenManager, logger, database }) {
|
|
3209
|
+
const disableDefaultAuthPolicy = config.getOptionalBoolean(
|
|
3210
|
+
"backend.auth.dangerouslyDisableDefaultAuthPolicy"
|
|
3211
|
+
) ?? false;
|
|
3212
|
+
const keyDuration = { hours: 1 };
|
|
3213
|
+
const keySource = await createPluginKeySource({
|
|
3214
|
+
config,
|
|
3215
|
+
database,
|
|
3216
|
+
logger,
|
|
3217
|
+
keyDuration
|
|
3218
|
+
});
|
|
3219
|
+
const userTokens = UserTokenHandler.create({
|
|
3220
|
+
discovery
|
|
3221
|
+
});
|
|
3222
|
+
const pluginTokens = PluginTokenHandler.create({
|
|
3223
|
+
ownPluginId: plugin.getId(),
|
|
3224
|
+
logger,
|
|
3225
|
+
keySource,
|
|
3226
|
+
keyDuration,
|
|
3227
|
+
discovery
|
|
3228
|
+
});
|
|
3229
|
+
const externalTokens = ExternalTokenHandler.create({
|
|
3230
|
+
ownPluginId: plugin.getId(),
|
|
3231
|
+
config,
|
|
3232
|
+
logger
|
|
3233
|
+
});
|
|
3234
|
+
return new DefaultAuthService(
|
|
3235
|
+
userTokens,
|
|
3236
|
+
pluginTokens,
|
|
3237
|
+
externalTokens,
|
|
3238
|
+
tokenManager,
|
|
3239
|
+
plugin.getId(),
|
|
3240
|
+
disableDefaultAuthPolicy,
|
|
3241
|
+
keySource
|
|
3242
|
+
);
|
|
2702
3243
|
}
|
|
2703
3244
|
});
|
|
2704
3245
|
|
|
3246
|
+
const authServiceFactory = authServiceFactory$1;
|
|
3247
|
+
|
|
2705
3248
|
const FIVE_MINUTES_MS = 5 * 60 * 1e3;
|
|
2706
3249
|
const BACKSTAGE_AUTH_COOKIE = "backstage-auth";
|
|
2707
3250
|
function getTokenFromRequest(req) {
|
|
@@ -2874,7 +3417,7 @@ class DefaultHttpAuthService {
|
|
|
2874
3417
|
}
|
|
2875
3418
|
}
|
|
2876
3419
|
}
|
|
2877
|
-
const httpAuthServiceFactory = backendPluginApi.createServiceFactory({
|
|
3420
|
+
const httpAuthServiceFactory$1 = backendPluginApi.createServiceFactory({
|
|
2878
3421
|
service: backendPluginApi.coreServices.httpAuth,
|
|
2879
3422
|
deps: {
|
|
2880
3423
|
auth: backendPluginApi.coreServices.auth,
|
|
@@ -2886,8 +3429,10 @@ const httpAuthServiceFactory = backendPluginApi.createServiceFactory({
|
|
|
2886
3429
|
}
|
|
2887
3430
|
});
|
|
2888
3431
|
|
|
3432
|
+
const httpAuthServiceFactory = httpAuthServiceFactory$1;
|
|
3433
|
+
|
|
2889
3434
|
const DEFAULT_TIMEOUT = { seconds: 5 };
|
|
2890
|
-
function createLifecycleMiddleware(options) {
|
|
3435
|
+
function createLifecycleMiddleware$1(options) {
|
|
2891
3436
|
const { lifecycle, startupRequestPauseTimeout = DEFAULT_TIMEOUT } = options;
|
|
2892
3437
|
let state = "init";
|
|
2893
3438
|
const waiting = /* @__PURE__ */ new Set();
|
|
@@ -3011,7 +3556,7 @@ function createCookieAuthRefreshMiddleware(options) {
|
|
|
3011
3556
|
return router;
|
|
3012
3557
|
}
|
|
3013
3558
|
|
|
3014
|
-
const httpRouterServiceFactory = backendPluginApi.createServiceFactory(
|
|
3559
|
+
const httpRouterServiceFactory$1 = backendPluginApi.createServiceFactory(
|
|
3015
3560
|
(options) => ({
|
|
3016
3561
|
service: backendPluginApi.coreServices.httpRouter,
|
|
3017
3562
|
initialization: "always",
|
|
@@ -3047,7 +3592,7 @@ const httpRouterServiceFactory = backendPluginApi.createServiceFactory(
|
|
|
3047
3592
|
config
|
|
3048
3593
|
});
|
|
3049
3594
|
router.use(createAuthIntegrationRouter({ auth }));
|
|
3050
|
-
router.use(createLifecycleMiddleware({ lifecycle }));
|
|
3595
|
+
router.use(createLifecycleMiddleware$1({ lifecycle }));
|
|
3051
3596
|
router.use(credentialsBarrier.middleware);
|
|
3052
3597
|
router.use(createCookieAuthRefreshMiddleware({ auth, httpAuth }));
|
|
3053
3598
|
return {
|
|
@@ -3062,76 +3607,11 @@ const httpRouterServiceFactory = backendPluginApi.createServiceFactory(
|
|
|
3062
3607
|
})
|
|
3063
3608
|
);
|
|
3064
3609
|
|
|
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
|
-
);
|
|
3610
|
+
const httpRouterServiceFactory = httpRouterServiceFactory$1;
|
|
3076
3611
|
|
|
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
|
-
});
|
|
3612
|
+
const createLifecycleMiddleware = createLifecycleMiddleware$1;
|
|
3133
3613
|
|
|
3134
|
-
const loggerServiceFactory = backendPluginApi.createServiceFactory({
|
|
3614
|
+
const loggerServiceFactory$1 = backendPluginApi.createServiceFactory({
|
|
3135
3615
|
service: backendPluginApi.coreServices.logger,
|
|
3136
3616
|
deps: {
|
|
3137
3617
|
rootLogger: backendPluginApi.coreServices.rootLogger,
|
|
@@ -3142,27 +3622,12 @@ const loggerServiceFactory = backendPluginApi.createServiceFactory({
|
|
|
3142
3622
|
}
|
|
3143
3623
|
});
|
|
3144
3624
|
|
|
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
|
-
});
|
|
3625
|
+
const loggerServiceFactory = loggerServiceFactory$1;
|
|
3161
3626
|
|
|
3162
3627
|
function normalizePath(path) {
|
|
3163
3628
|
return `${trimEnd__default.default(path, "/")}/`;
|
|
3164
3629
|
}
|
|
3165
|
-
class DefaultRootHttpRouter {
|
|
3630
|
+
let DefaultRootHttpRouter$1 = class DefaultRootHttpRouter {
|
|
3166
3631
|
#indexPath;
|
|
3167
3632
|
#router = express.Router();
|
|
3168
3633
|
#namedRoutes = express.Router();
|
|
@@ -3223,12 +3688,12 @@ class DefaultRootHttpRouter {
|
|
|
3223
3688
|
}
|
|
3224
3689
|
return void 0;
|
|
3225
3690
|
}
|
|
3226
|
-
}
|
|
3691
|
+
};
|
|
3227
3692
|
|
|
3228
3693
|
function defaultConfigure({ applyDefaults }) {
|
|
3229
3694
|
applyDefaults();
|
|
3230
3695
|
}
|
|
3231
|
-
const rootHttpRouterServiceFactory = backendPluginApi.createServiceFactory(
|
|
3696
|
+
const rootHttpRouterServiceFactory$1 = backendPluginApi.createServiceFactory(
|
|
3232
3697
|
(options) => ({
|
|
3233
3698
|
service: backendPluginApi.coreServices.rootHttpRouter,
|
|
3234
3699
|
deps: {
|
|
@@ -3240,12 +3705,12 @@ const rootHttpRouterServiceFactory = backendPluginApi.createServiceFactory(
|
|
|
3240
3705
|
const { indexPath, configure = defaultConfigure } = options ?? {};
|
|
3241
3706
|
const logger = rootLogger.child({ service: "rootHttpRouter" });
|
|
3242
3707
|
const app = express__default.default();
|
|
3243
|
-
const router = DefaultRootHttpRouter.create({ indexPath });
|
|
3244
|
-
const middleware = MiddlewareFactory.create({ config, logger });
|
|
3708
|
+
const router = DefaultRootHttpRouter$1.create({ indexPath });
|
|
3709
|
+
const middleware = MiddlewareFactory$1.create({ config, logger });
|
|
3245
3710
|
const routes = router.handler();
|
|
3246
|
-
const server = await createHttpServer(
|
|
3711
|
+
const server = await createHttpServer$1(
|
|
3247
3712
|
app,
|
|
3248
|
-
readHttpServerOptions(config.getOptionalConfig("backend")),
|
|
3713
|
+
readHttpServerOptions$1(config.getOptionalConfig("backend")),
|
|
3249
3714
|
{ logger }
|
|
3250
3715
|
);
|
|
3251
3716
|
configure({
|
|
@@ -3273,128 +3738,46 @@ const rootHttpRouterServiceFactory = backendPluginApi.createServiceFactory(
|
|
|
3273
3738
|
})
|
|
3274
3739
|
);
|
|
3275
3740
|
|
|
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 });
|
|
3741
|
+
const rootHttpRouterServiceFactory = rootHttpRouterServiceFactory$1;
|
|
3742
|
+
|
|
3743
|
+
class DefaultRootHttpRouter {
|
|
3744
|
+
constructor(impl) {
|
|
3745
|
+
this.impl = impl;
|
|
3287
3746
|
}
|
|
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
|
-
);
|
|
3747
|
+
static create(options) {
|
|
3748
|
+
return new DefaultRootHttpRouter(DefaultRootHttpRouter$1.create(options));
|
|
3305
3749
|
}
|
|
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 });
|
|
3750
|
+
use(path, handler) {
|
|
3751
|
+
this.impl.use(path, handler);
|
|
3313
3752
|
}
|
|
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
|
-
);
|
|
3753
|
+
handler() {
|
|
3754
|
+
return this.impl.handler();
|
|
3333
3755
|
}
|
|
3334
3756
|
}
|
|
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
3757
|
|
|
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
|
-
});
|
|
3758
|
+
const rootLoggerServiceFactory = rootLoggerServiceFactory$1;
|
|
3382
3759
|
|
|
3383
|
-
const
|
|
3384
|
-
service: backendPluginApi.coreServices.
|
|
3760
|
+
const schedulerServiceFactory = backendPluginApi.createServiceFactory({
|
|
3761
|
+
service: backendPluginApi.coreServices.scheduler,
|
|
3385
3762
|
deps: {
|
|
3386
|
-
|
|
3763
|
+
plugin: backendPluginApi.coreServices.pluginMetadata,
|
|
3764
|
+
databaseManager: backendPluginApi.coreServices.database,
|
|
3387
3765
|
logger: backendPluginApi.coreServices.logger
|
|
3388
3766
|
},
|
|
3389
|
-
async factory({
|
|
3390
|
-
return
|
|
3391
|
-
|
|
3767
|
+
async factory({ plugin, databaseManager, logger }) {
|
|
3768
|
+
return backendTasks.TaskScheduler.forPlugin({
|
|
3769
|
+
pluginId: plugin.getId(),
|
|
3770
|
+
databaseManager,
|
|
3392
3771
|
logger
|
|
3393
3772
|
});
|
|
3394
3773
|
}
|
|
3395
3774
|
});
|
|
3396
3775
|
|
|
3397
3776
|
class DefaultUserInfoService {
|
|
3777
|
+
discovery;
|
|
3778
|
+
constructor(options) {
|
|
3779
|
+
this.discovery = options.discovery;
|
|
3780
|
+
}
|
|
3398
3781
|
async getUserInfo(credentials) {
|
|
3399
3782
|
const internalCredentials = toInternalBackstageCredentials(credentials);
|
|
3400
3783
|
if (internalCredentials.principal.type !== "user") {
|
|
@@ -3403,42 +3786,51 @@ class DefaultUserInfoService {
|
|
|
3403
3786
|
if (!internalCredentials.token) {
|
|
3404
3787
|
throw new Error("User credentials is unexpectedly missing token");
|
|
3405
3788
|
}
|
|
3406
|
-
const { sub: userEntityRef, ent:
|
|
3789
|
+
const { sub: userEntityRef, ent: tokenEnt } = jose.decodeJwt(
|
|
3407
3790
|
internalCredentials.token
|
|
3408
3791
|
);
|
|
3409
3792
|
if (typeof userEntityRef !== "string") {
|
|
3410
3793
|
throw new Error("User entity ref must be a string");
|
|
3411
3794
|
}
|
|
3412
|
-
|
|
3795
|
+
let ownershipEntityRefs = tokenEnt;
|
|
3796
|
+
if (!ownershipEntityRefs) {
|
|
3797
|
+
const userInfoResp = await fetch__default.default(
|
|
3798
|
+
`${await this.discovery.getBaseUrl("auth")}/v1/userinfo`,
|
|
3799
|
+
{
|
|
3800
|
+
headers: {
|
|
3801
|
+
Authorization: `Bearer ${internalCredentials.token}`
|
|
3802
|
+
}
|
|
3803
|
+
}
|
|
3804
|
+
);
|
|
3805
|
+
if (!userInfoResp.ok) {
|
|
3806
|
+
throw await errors.ResponseError.fromResponse(userInfoResp);
|
|
3807
|
+
}
|
|
3808
|
+
const {
|
|
3809
|
+
claims: { ent }
|
|
3810
|
+
} = await userInfoResp.json();
|
|
3811
|
+
ownershipEntityRefs = ent;
|
|
3812
|
+
}
|
|
3813
|
+
if (!ownershipEntityRefs) {
|
|
3814
|
+
throw new Error("Ownership entity refs can not be determined");
|
|
3815
|
+
} else if (!Array.isArray(ownershipEntityRefs) || ownershipEntityRefs.some((ref) => typeof ref !== "string")) {
|
|
3413
3816
|
throw new Error("Ownership entity refs must be an array of strings");
|
|
3414
3817
|
}
|
|
3415
3818
|
return { userEntityRef, ownershipEntityRefs };
|
|
3416
3819
|
}
|
|
3417
3820
|
}
|
|
3418
|
-
const userInfoServiceFactory = backendPluginApi.createServiceFactory({
|
|
3419
|
-
service: backendPluginApi.coreServices.userInfo,
|
|
3420
|
-
deps: {},
|
|
3421
|
-
async factory() {
|
|
3422
|
-
return new DefaultUserInfoService();
|
|
3423
|
-
}
|
|
3424
|
-
});
|
|
3425
3821
|
|
|
3426
|
-
const
|
|
3427
|
-
service: backendPluginApi.coreServices.
|
|
3822
|
+
const userInfoServiceFactory$1 = backendPluginApi.createServiceFactory({
|
|
3823
|
+
service: backendPluginApi.coreServices.userInfo,
|
|
3428
3824
|
deps: {
|
|
3429
|
-
|
|
3430
|
-
databaseManager: backendPluginApi.coreServices.database,
|
|
3431
|
-
logger: backendPluginApi.coreServices.logger
|
|
3825
|
+
discovery: backendPluginApi.coreServices.discovery
|
|
3432
3826
|
},
|
|
3433
|
-
async factory({
|
|
3434
|
-
return
|
|
3435
|
-
pluginId: plugin.getId(),
|
|
3436
|
-
databaseManager,
|
|
3437
|
-
logger
|
|
3438
|
-
});
|
|
3827
|
+
async factory({ discovery }) {
|
|
3828
|
+
return new DefaultUserInfoService({ discovery });
|
|
3439
3829
|
}
|
|
3440
3830
|
});
|
|
3441
3831
|
|
|
3832
|
+
const userInfoServiceFactory = userInfoServiceFactory$1;
|
|
3833
|
+
|
|
3442
3834
|
exports.DefaultRootHttpRouter = DefaultRootHttpRouter;
|
|
3443
3835
|
exports.HostDiscovery = HostDiscovery;
|
|
3444
3836
|
exports.MiddlewareFactory = MiddlewareFactory;
|