move the auth services to backend-defaults

Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
Fredrik Adelöw
2024-06-14 12:09:48 +02:00
parent 2d3977f78c
commit 9539a0b0a9
55 changed files with 1026 additions and 455 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-common': patch
---
Import utility functions from `backend-defaults` instead of `backend-app-api`
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-app-api': patch
---
Deprecated `authServiceFactory`, `httpAuthServiceFactory`, and `userInfoServiceFactory`. Please import them from `@backstage/backend-defaults/auth`, `@backstage/backend-defaults/httpAuth`, and `@backstage/backend-defaults/userInfo` respectively instead.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-plugin-api': patch
---
Improved `coreServices` doc comments
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-defaults': patch
---
Added `@backstage/backend-defaults/auth`, `@backstage/backend-defaults/httpAuth`, and `@backstage/backend-defaults/userInfo` to house their respective backend service factories. You should now import these services from those new locations, instead of `@backstage/backend-app-api`.
+3 -3
View File
@@ -45,7 +45,7 @@ import { transport } from 'winston';
import { UrlReaderService } from '@backstage/backend-plugin-api';
import { UserInfoService } from '@backstage/backend-plugin-api';
// @public (undocumented)
// @public @deprecated (undocumented)
export const authServiceFactory: () => ServiceFactory<AuthService, 'plugin'>;
// @public (undocumented)
@@ -151,7 +151,7 @@ export class HostDiscovery implements DiscoveryService {
getExternalBaseUrl(pluginId: string): Promise<string>;
}
// @public (undocumented)
// @public @deprecated (undocumented)
export const httpAuthServiceFactory: () => ServiceFactory<
HttpAuthService,
'plugin'
@@ -344,7 +344,7 @@ export const urlReaderServiceFactory: () => ServiceFactory<
'plugin'
>;
// @public (undocumented)
// @public @deprecated (undocumented)
export const userInfoServiceFactory: () => ServiceFactory<
UserInfoService,
'plugin'
@@ -14,72 +14,11 @@
* limitations under the License.
*/
import {
coreServices,
createServiceFactory,
} from '@backstage/backend-plugin-api';
import { DefaultAuthService } from './DefaultAuthService';
import { ExternalTokenHandler } from './external/ExternalTokenHandler';
import { PluginTokenHandler } from './plugin/PluginTokenHandler';
import { createPluginKeySource } from './plugin/keys/createPluginKeySource';
import { UserTokenHandler } from './user/UserTokenHandler';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { authServiceFactory as _authServiceFactory } from '../../../../../backend-defaults/src/entrypoints/auth';
/** @public */
export const authServiceFactory = createServiceFactory({
service: coreServices.auth,
deps: {
config: coreServices.rootConfig,
logger: coreServices.rootLogger,
discovery: coreServices.discovery,
plugin: coreServices.pluginMetadata,
database: coreServices.database,
// Re-using the token manager makes sure that we use the same generated keys for
// development as plugins that have not yet been migrated. It's important that this
// keeps working as long as there are plugins that have not been migrated to the
// new auth services in the new backend system.
tokenManager: coreServices.tokenManager,
},
async factory({ config, discovery, plugin, tokenManager, logger, database }) {
const disableDefaultAuthPolicy =
config.getOptionalBoolean(
'backend.auth.dangerouslyDisableDefaultAuthPolicy',
) ?? false;
const keyDuration = { hours: 1 };
const keySource = await createPluginKeySource({
config,
database,
logger,
keyDuration,
});
const userTokens = UserTokenHandler.create({
discovery,
});
const pluginTokens = PluginTokenHandler.create({
ownPluginId: plugin.getId(),
logger,
keySource,
keyDuration,
discovery,
});
const externalTokens = ExternalTokenHandler.create({
ownPluginId: plugin.getId(),
config,
logger,
});
return new DefaultAuthService(
userTokens,
pluginTokens,
externalTokens,
tokenManager,
plugin.getId(),
disableDefaultAuthPolicy,
keySource,
);
},
});
/**
* @public
* @deprecated Please import from `@backstage/backend-defaults/auth` instead.
*/
export const authServiceFactory = _authServiceFactory;
@@ -14,4 +14,7 @@
* limitations under the License.
*/
export * from './auth';
export * from './httpAuth';
export * from './scheduler';
export * from './userInfo';
@@ -14,273 +14,11 @@
* limitations under the License.
*/
import {
AuthService,
BackstageCredentials,
BackstagePrincipalTypes,
BackstageUserPrincipal,
DiscoveryService,
HttpAuthService,
coreServices,
createServiceFactory,
} from '@backstage/backend-plugin-api';
import { AuthenticationError, NotAllowedError } from '@backstage/errors';
import { parse as parseCookie } from 'cookie';
import { Request, Response } from 'express';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { httpAuthServiceFactory as _httpAuthServiceFactory } from '../../../../../backend-defaults/src/entrypoints/httpAuth';
const FIVE_MINUTES_MS = 5 * 60 * 1000;
const BACKSTAGE_AUTH_COOKIE = 'backstage-auth';
function getTokenFromRequest(req: Request) {
// TODO: support multiple auth headers (iterate rawHeaders)
const authHeader = req.headers.authorization;
if (typeof authHeader === 'string') {
const matches = authHeader.match(/^Bearer[ ]+(\S+)$/i);
const token = matches?.[1];
if (token) {
return token;
}
}
return undefined;
}
function getCookieFromRequest(req: Request) {
const cookieHeader = req.headers.cookie;
if (cookieHeader) {
const cookies = parseCookie(cookieHeader);
const token = cookies[BACKSTAGE_AUTH_COOKIE];
if (token) {
return token;
}
}
return undefined;
}
function willExpireSoon(expiresAt: Date) {
return Date.now() + FIVE_MINUTES_MS > expiresAt.getTime();
}
const credentialsSymbol = Symbol('backstage-credentials');
const limitedCredentialsSymbol = Symbol('backstage-limited-credentials');
type RequestWithCredentials = Request & {
[credentialsSymbol]?: Promise<BackstageCredentials>;
[limitedCredentialsSymbol]?: Promise<BackstageCredentials>;
};
class DefaultHttpAuthService implements HttpAuthService {
readonly #auth: AuthService;
readonly #discovery: DiscoveryService;
readonly #pluginId: string;
constructor(
auth: AuthService,
discovery: DiscoveryService,
pluginId: string,
) {
this.#auth = auth;
this.#discovery = discovery;
this.#pluginId = pluginId;
}
async #extractCredentialsFromRequest(req: Request) {
const token = getTokenFromRequest(req);
if (!token) {
return await this.#auth.getNoneCredentials();
}
return await this.#auth.authenticate(token);
}
async #extractLimitedCredentialsFromRequest(req: Request) {
const token = getTokenFromRequest(req);
if (token) {
return await this.#auth.authenticate(token, {
allowLimitedAccess: true,
});
}
const cookie = getCookieFromRequest(req);
if (cookie) {
return await this.#auth.authenticate(cookie, {
allowLimitedAccess: true,
});
}
return await this.#auth.getNoneCredentials();
}
async #getCredentials(req: RequestWithCredentials) {
return (req[credentialsSymbol] ??=
this.#extractCredentialsFromRequest(req));
}
async #getLimitedCredentials(req: RequestWithCredentials) {
return (req[limitedCredentialsSymbol] ??=
this.#extractLimitedCredentialsFromRequest(req));
}
async credentials<TAllowed extends keyof BackstagePrincipalTypes = 'unknown'>(
req: Request,
options?: {
allow?: Array<TAllowed>;
allowLimitedAccess?: boolean;
},
): Promise<BackstageCredentials<BackstagePrincipalTypes[TAllowed]>> {
// Limited and full credentials are treated as two separate cases, this lets
// us avoid internal dependencies between the AuthService and
// HttpAuthService implementations
const credentials = options?.allowLimitedAccess
? await this.#getLimitedCredentials(req)
: await this.#getCredentials(req);
const allowed = options?.allow;
if (!allowed) {
return credentials as any;
}
if (this.#auth.isPrincipal(credentials, 'none')) {
if (allowed.includes('none' as TAllowed)) {
return credentials as any;
}
throw new AuthenticationError('Missing credentials');
} else if (this.#auth.isPrincipal(credentials, 'user')) {
if (allowed.includes('user' as TAllowed)) {
return credentials as any;
}
throw new NotAllowedError(
`This endpoint does not allow 'user' credentials`,
);
} else if (this.#auth.isPrincipal(credentials, 'service')) {
if (allowed.includes('service' as TAllowed)) {
return credentials as any;
}
throw new NotAllowedError(
`This endpoint does not allow 'service' credentials`,
);
}
throw new NotAllowedError(
'Unknown principal type, this should never happen',
);
}
async issueUserCookie(
res: Response,
options?: { credentials?: BackstageCredentials },
): Promise<{ expiresAt: Date }> {
if (res.headersSent) {
throw new Error('Failed to issue user cookie, headers were already sent');
}
let credentials: BackstageCredentials<BackstageUserPrincipal>;
if (options?.credentials) {
if (this.#auth.isPrincipal(options.credentials, 'none')) {
res.clearCookie(
BACKSTAGE_AUTH_COOKIE,
await this.#getCookieOptions(res.req),
);
return { expiresAt: new Date() };
}
if (!this.#auth.isPrincipal(options.credentials, 'user')) {
throw new AuthenticationError(
'Refused to issue cookie for non-user principal',
);
}
credentials = options.credentials;
} else {
credentials = await this.credentials(res.req, { allow: ['user'] });
}
const existingExpiresAt = await this.#existingCookieExpiration(res.req);
if (existingExpiresAt && !willExpireSoon(existingExpiresAt)) {
return { expiresAt: existingExpiresAt };
}
const { token, expiresAt } = await this.#auth.getLimitedUserToken(
credentials,
);
if (!token) {
throw new Error('User credentials is unexpectedly missing token');
}
res.cookie(BACKSTAGE_AUTH_COOKIE, token, {
...(await this.#getCookieOptions(res.req)),
expires: expiresAt,
});
return { expiresAt };
}
async #getCookieOptions(_req: Request): Promise<{
domain: string;
httpOnly: true;
secure: boolean;
priority: 'high';
sameSite: 'none' | 'lax';
}> {
// TODO: eventually we should read from `${req.protocol}://${req.hostname}`
// once https://github.com/backstage/backstage/issues/24169 has landed
const externalBaseUrlStr = await this.#discovery.getExternalBaseUrl(
this.#pluginId,
);
const externalBaseUrl = new URL(externalBaseUrlStr);
const secure =
externalBaseUrl.protocol === 'https:' ||
externalBaseUrl.hostname === 'localhost';
return {
domain: externalBaseUrl.hostname,
httpOnly: true,
secure,
priority: 'high',
sameSite: secure ? 'none' : 'lax',
};
}
async #existingCookieExpiration(req: Request): Promise<Date | undefined> {
const existingCookie = getCookieFromRequest(req);
if (!existingCookie) {
return undefined;
}
try {
const existingCredentials = await this.#auth.authenticate(
existingCookie,
{
allowLimitedAccess: true,
},
);
if (!this.#auth.isPrincipal(existingCredentials, 'user')) {
return undefined;
}
return existingCredentials.expiresAt;
} catch (error) {
if (error.name === 'AuthenticationError') {
return undefined;
}
throw error;
}
}
}
/** @public */
export const httpAuthServiceFactory = createServiceFactory({
service: coreServices.httpAuth,
deps: {
auth: coreServices.auth,
discovery: coreServices.discovery,
plugin: coreServices.pluginMetadata,
},
async factory({ auth, discovery, plugin }) {
return new DefaultHttpAuthService(auth, discovery, plugin.getId());
},
});
/**
* @public
* @deprecated Please import from `@backstage/backend-defaults/httpAuth` instead.
*/
export const httpAuthServiceFactory = _httpAuthServiceFactory;
@@ -14,12 +14,10 @@
* limitations under the License.
*/
export * from './auth';
export * from './cache';
export * from './config';
export * from './database';
export * from './discovery';
export * from './httpAuth';
export * from './httpRouter';
export * from './identity';
export * from './lifecycle';
@@ -30,6 +28,5 @@ export * from './rootLifecycle';
export * from './rootLogger';
export * from './tokenManager';
export * from './urlReader';
export * from './userInfo';
export * from './deprecated';
@@ -14,90 +14,11 @@
* limitations under the License.
*/
import {
UserInfoService,
BackstageUserInfo,
coreServices,
createServiceFactory,
DiscoveryService,
BackstageCredentials,
} from '@backstage/backend-plugin-api';
import { ResponseError } from '@backstage/errors';
import { decodeJwt } from 'jose';
import fetch from 'node-fetch';
import { toInternalBackstageCredentials } from '../auth/helpers';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { userInfoServiceFactory as _userInfoServiceFactory } from '../../../../../backend-defaults/src/entrypoints/userInfo';
type Options = {
discovery: DiscoveryService;
};
export class DefaultUserInfoService implements UserInfoService {
private readonly discovery: DiscoveryService;
constructor(options: Options) {
this.discovery = options.discovery;
}
async getUserInfo(
credentials: BackstageCredentials,
): Promise<BackstageUserInfo> {
const internalCredentials = toInternalBackstageCredentials(credentials);
if (internalCredentials.principal.type !== 'user') {
throw new Error('Only user credentials are supported');
}
if (!internalCredentials.token) {
throw new Error('User credentials is unexpectedly missing token');
}
const { sub: userEntityRef, ent: tokenEnt } = decodeJwt(
internalCredentials.token,
);
if (typeof userEntityRef !== 'string') {
throw new Error('User entity ref must be a string');
}
let ownershipEntityRefs = tokenEnt;
if (!ownershipEntityRefs) {
const userInfoResp = await fetch(
`${await this.discovery.getBaseUrl('auth')}/v1/userinfo`,
{
headers: {
Authorization: `Bearer ${internalCredentials.token}`,
},
},
);
if (!userInfoResp.ok) {
throw await ResponseError.fromResponse(userInfoResp);
}
const {
claims: { ent },
} = await userInfoResp.json();
ownershipEntityRefs = ent;
}
if (!ownershipEntityRefs) {
throw new Error('Ownership entity refs can not be determined');
} else if (
!Array.isArray(ownershipEntityRefs) ||
ownershipEntityRefs.some(ref => typeof ref !== 'string')
) {
throw new Error('Ownership entity refs must be an array of strings');
}
return { userEntityRef, ownershipEntityRefs };
}
}
/** @public */
export const userInfoServiceFactory = createServiceFactory({
service: coreServices.userInfo,
deps: {
discovery: coreServices.discovery,
},
async factory({ discovery }) {
return new DefaultUserInfoService({ discovery });
},
});
/**
* @public
* @deprecated Please import from `@backstage/backend-defaults/userInfo` instead.
*/
export const userInfoServiceFactory = _userInfoServiceFactory;
@@ -35,7 +35,7 @@ import {
createCredentialsWithUserPrincipal,
createCredentialsWithNonePrincipal,
toInternalBackstageCredentials,
} from '../../../backend-app-api/src/services/implementations/auth/helpers';
} from '../../../backend-defaults/src/entrypoints/auth/helpers';
// TODO is this circular thingy a problem? Test in e2e
import {
type IdentityApiGetIdentityRequest,
@@ -0,0 +1,13 @@
## API Report File for "@backstage/backend-defaults"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { AuthService } from '@backstage/backend-plugin-api';
import { ServiceFactory } from '@backstage/backend-plugin-api';
// @public
export const authServiceFactory: () => ServiceFactory<AuthService, 'plugin'>;
// (No @packageDocumentation comment for this package)
```
@@ -0,0 +1,16 @@
## API Report File for "@backstage/backend-defaults"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { HttpAuthService } from '@backstage/backend-plugin-api';
import { ServiceFactory } from '@backstage/backend-plugin-api';
// @public
export const httpAuthServiceFactory: () => ServiceFactory<
HttpAuthService,
'plugin'
>;
// (No @packageDocumentation comment for this package)
```
@@ -0,0 +1,16 @@
## API Report File for "@backstage/backend-defaults"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { ServiceFactory } from '@backstage/backend-plugin-api';
import { UserInfoService } from '@backstage/backend-plugin-api';
// @public
export const userInfoServiceFactory: () => ServiceFactory<
UserInfoService,
'plugin'
>;
// (No @packageDocumentation comment for this package)
```
+277
View File
@@ -15,6 +15,283 @@
*/
export interface Config {
backend?: {
/**
* Options used by the default auth, httpAuth and userInfo services.
*/
auth?: {
/**
* This disables the otherwise default auth policy, which requires all
* requests to be authenticated with either user or service credentials.
*
* Disabling this check means that the backend will no longer block
* unauthenticated requests, but instead allow them to pass through to
* plugins.
*
* If permissions are enabled, unauthenticated requests will be treated
* exactly as such, leaving it to the permission policy to determine what
* permissions should be allowed for an unauthenticated identity. Note
* that this will also apply to service-to-service calls between plugins
* unless you configure credentials for service calls.
*/
dangerouslyDisableDefaultAuthPolicy?: boolean;
/** Controls how to store keys for plugin-to-plugin auth */
pluginKeyStore?:
| { type: 'database' }
| {
type: 'static';
static: {
/**
* Must be declared at least once and the first one will be used for signing.
*/
keys: Array<{
/**
* Path to the public key file in the SPKI format. Should be an absolute path.
*/
publicKeyFile: string;
/**
* Path to the matching private key file in the PKCS#8 format. Should be an absolute path.
*
* The first array entry must specify a private key file, the rest must not.
*/
privateKeyFile?: string;
/**
* ID to uniquely identify this key within the JWK set.
*/
keyId: string;
/**
* JWS "alg" (Algorithm) Header Parameter value. Defaults to ES256.
* Must match the algorithm used to generate the keys in the provided files
*/
algorithm?: string;
}>;
};
};
/**
* Configures methods of external access, ie ways for callers outside of
* the Backstage ecosystem to get authorized for access to APIs that do
* not permit unauthorized access.
*/
externalAccess: Array<
| {
/**
* This is the legacy service-to-service access method, where a set
* of static keys were shared among plugins and used for symmetric
* signing and verification. These correspond to the old
* `backend.auth.keys` set and retain their behavior for backwards
* compatibility. Please migrate to other access methods when
* possible.
*
* Callers generate JWT tokens with the following payload:
*
* ```json
* {
* "sub": "backstage-plugin",
* "exp": <epoch seconds one hour in the future>
* }
* ```
*
* And sign them with HS256, using the base64 decoded secret. The
* tokens are then passed along with requests in the Authorization
* header:
*
* ```
* Authorization: Bearer eyJhbGciOiJIUzI...
* ```
*/
type: 'legacy';
options: {
/**
* Any set of base64 encoded random bytes to be used as both the
* signing and verification key. Should be sufficiently long so as
* not to be easy to guess by brute force.
*
* Can be generated eg using
*
* ```sh
* node -p 'require("crypto").randomBytes(24).toString("base64")'
* ```
*
* @visibility secret
*/
secret: string;
/**
* Sets the subject of the principal, when matching this token.
* Useful for debugging and tracking purposes.
*/
subject: string;
};
/**
* Restricts what types of access that are permitted for this access
* method. If no access restrictions are given, it'll have unlimited
* access. This access restriction applies for the framework level;
* individual plugins may have their own access control mechanisms
* on top of this.
*/
accessRestrictions?: Array<{
/**
* Permit access to make requests to this plugin.
*
* Can be further refined by setting additional fields below.
*/
plugin: string;
/**
* If given, this method is limited to only performing actions
* with these named permissions in this plugin.
*
* Note that this only applies where permissions checks are
* enabled in the first place. Endpoints that are not protected by
* the permissions system at all, are not affected by this
* setting.
*/
permission?: string | Array<string>;
/**
* If given, this method is limited to only performing actions
* whose permissions have these attributes.
*
* Note that this only applies where permissions checks are
* enabled in the first place. Endpoints that are not protected by
* the permissions system at all, are not affected by this
* setting.
*/
permissionAttribute?: {
/**
* One of more of 'create', 'read', 'update', or 'delete'.
*/
action?: string | Array<string>;
};
}>;
}
| {
/**
* This access method consists of random static tokens that can be
* handed out to callers.
*
* The tokens are then passed along verbatim with requests in the
* Authorization header:
*
* ```
* Authorization: Bearer eZv5o+fW3KnR3kVabMW4ZcDNLPl8nmMW
* ```
*/
type: 'static';
options: {
/**
* A raw token that can be any string, but for security reasons
* should be sufficiently long so as not to be easy to guess by
* brute force.
*
* Can be generated eg using
*
* ```sh
* node -p 'require("crypto").randomBytes(24).toString("base64")'
* ```
*
* Since the tokens can be any string, you are free to add
* additional identifying data to them if you like. For example,
* adding a `freben-local-dev-` prefix for debugging purposes to a
* token that you know will be handed out for use as a personal
* access token during development.
*
* @visibility secret
*/
token: string;
/**
* Sets the subject of the principal, when matching this token.
* Useful for debugging and tracking purposes.
*/
subject: string;
};
/**
* Restricts what types of access that are permitted for this access
* method. If no access restrictions are given, it'll have unlimited
* access. This access restriction applies for the framework level;
* individual plugins may have their own access control mechanisms
* on top of this.
*/
accessRestrictions?: Array<{
/**
* Permit access to make requests to this plugin.
*
* Can be further refined by setting additional fields below.
*/
plugin: string;
/**
* If given, this method is limited to only performing actions
* with these named permissions in this plugin.
*
* Note that this only applies where permissions checks are
* enabled in the first place. Endpoints that are not protected by
* the permissions system at all, are not affected by this
* setting.
*/
permission?: string | Array<string>;
/**
* If given, this method is limited to only performing actions
* whose permissions have these attributes.
*
* Note that this only applies where permissions checks are
* enabled in the first place. Endpoints that are not protected by
* the permissions system at all, are not affected by this
* setting.
*/
permissionAttribute?: {
/**
* One of more of 'create', 'read', 'update', or 'delete'.
*/
action?: string | Array<string>;
};
}>;
}
| {
/**
* This access method consists of a JWKS endpoint that can be used to
* verify JWT tokens.
*
* Callers generate JWT tokens via 3rd party tooling
* and pass them in the Authorization header:
*
* ```
* Authorization: Bearer eZv5o+fW3KnR3kVabMW4ZcDNLPl8nmMW
* ```
*/
type: 'jwks';
options: {
/**
* The full URL of the JWKS endpoint.
*/
url: string;
/**
* Sets the algorithm(s) that should be used to verify the JWT tokens.
* The passed JWTs must have been signed using one of the listed algorithms.
*/
algorithm?: string | string[];
/**
* Sets the issuer(s) that should be used to verify the JWT tokens.
* Passed JWTs must have an `iss` claim which matches one of the specified issuers.
*/
issuer?: string | string[];
/**
* Sets the audience(s) that should be used to verify the JWT tokens.
* The passed JWTs must have an "aud" claim that matches one of the audiences specified,
* or have no audience specified.
*/
audience?: string | string[];
/**
* Sets an optional subject prefix. Passes the subject to called plugins.
* Useful for debugging and tracking purposes.
*/
subjectPrefix?: string;
};
}
>;
};
};
/**
* Options used by the default discovery service.
*/
+17
View File
@@ -20,21 +20,27 @@
"license": "Apache-2.0",
"exports": {
".": "./src/index.ts",
"./auth": "./src/entrypoints/auth/index.ts",
"./cache": "./src/entrypoints/cache/index.ts",
"./database": "./src/entrypoints/database/index.ts",
"./discovery": "./src/entrypoints/discovery/index.ts",
"./httpAuth": "./src/entrypoints/httpAuth/index.ts",
"./lifecycle": "./src/entrypoints/lifecycle/index.ts",
"./permissions": "./src/entrypoints/permissions/index.ts",
"./rootConfig": "./src/entrypoints/rootConfig/index.ts",
"./rootLifecycle": "./src/entrypoints/rootLifecycle/index.ts",
"./scheduler": "./src/entrypoints/scheduler/index.ts",
"./urlReader": "./src/entrypoints/urlReader/index.ts",
"./userInfo": "./src/entrypoints/userInfo/index.ts",
"./package.json": "./package.json"
},
"main": "src/index.ts",
"types": "src/index.ts",
"typesVersions": {
"*": {
"auth": [
"src/entrypoints/auth/index.ts"
],
"cache": [
"src/entrypoints/cache/index.ts"
],
@@ -44,6 +50,9 @@
"discovery": [
"src/entrypoints/discovery/index.ts"
],
"httpAuth": [
"src/entrypoints/httpAuth/index.ts"
],
"lifecycle": [
"src/entrypoints/lifecycle/index.ts"
],
@@ -62,6 +71,9 @@
"urlReader": [
"src/entrypoints/urlReader/index.ts"
],
"userInfo": [
"src/entrypoints/userInfo/index.ts"
],
"package.json": [
"package.json"
]
@@ -88,6 +100,7 @@
"@aws-sdk/credential-providers": "^3.350.0",
"@aws-sdk/types": "^3.347.0",
"@backstage/backend-app-api": "workspace:^",
"@backstage/backend-common": "workspace:^",
"@backstage/backend-dev-utils": "workspace:^",
"@backstage/backend-plugin-api": "workspace:^",
"@backstage/config": "workspace:^",
@@ -95,6 +108,7 @@
"@backstage/errors": "workspace:^",
"@backstage/integration": "workspace:^",
"@backstage/integration-aws-node": "workspace:^",
"@backstage/plugin-auth-node": "workspace:^",
"@backstage/plugin-events-node": "workspace:^",
"@backstage/plugin-permission-node": "workspace:^",
"@backstage/types": "workspace:^",
@@ -107,10 +121,13 @@
"base64-stream": "^1.0.0",
"better-sqlite3": "^9.0.0",
"concat-stream": "^2.0.0",
"cookie": "^0.6.0",
"cron": "^3.0.0",
"express": "^4.17.1",
"fs-extra": "^11.2.0",
"git-url-parse": "^14.0.0",
"isomorphic-git": "^1.23.0",
"jose": "^5.0.0",
"keyv": "^4.5.2",
"knex": "^3.0.0",
"lodash": "^4.17.21",
@@ -16,26 +16,26 @@
import {
Backend,
authServiceFactory,
createSpecializedBackend,
httpAuthServiceFactory,
httpRouterServiceFactory,
identityServiceFactory,
loggerServiceFactory,
rootHttpRouterServiceFactory,
rootLoggerServiceFactory,
tokenManagerServiceFactory,
userInfoServiceFactory,
} from '@backstage/backend-app-api';
import { authServiceFactory } from '@backstage/backend-defaults/auth';
import { cacheServiceFactory } from '@backstage/backend-defaults/cache';
import { databaseServiceFactory } from '@backstage/backend-defaults/database';
import { discoveryServiceFactory } from '@backstage/backend-defaults/discovery';
import { httpAuthServiceFactory } from '@backstage/backend-defaults/httpAuth';
import { lifecycleServiceFactory } from '@backstage/backend-defaults/lifecycle';
import { permissionsServiceFactory } from '@backstage/backend-defaults/permissions';
import { rootConfigServiceFactory } from '@backstage/backend-defaults/rootConfig';
import { rootLifecycleServiceFactory } from '@backstage/backend-defaults/rootLifecycle';
import { schedulerServiceFactory } from '@backstage/backend-defaults/scheduler';
import { urlReaderServiceFactory } from '@backstage/backend-defaults/urlReader';
import { userInfoServiceFactory } from '@backstage/backend-defaults/userInfo';
import { eventsServiceFactory } from '@backstage/plugin-events-node';
export const defaultServiceFactories = [
@@ -19,6 +19,7 @@ import {
mockServices,
setupRequestMockHandlers,
} from '@backstage/backend-test-utils';
import { tokenManagerServiceFactory } from '@backstage/backend-app-api';
import { authServiceFactory } from './authServiceFactory';
import { base64url, decodeJwt } from 'jose';
import { discoveryServiceFactory } from '../discovery';
@@ -26,7 +27,6 @@ import {
BackstageServicePrincipal,
BackstageUserPrincipal,
} from '@backstage/backend-plugin-api';
import { tokenManagerServiceFactory } from '../tokenManager';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { InternalBackstageCredentials } from './types';
@@ -0,0 +1,89 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
coreServices,
createServiceFactory,
} from '@backstage/backend-plugin-api';
import { DefaultAuthService } from './DefaultAuthService';
import { ExternalTokenHandler } from './external/ExternalTokenHandler';
import { PluginTokenHandler } from './plugin/PluginTokenHandler';
import { createPluginKeySource } from './plugin/keys/createPluginKeySource';
import { UserTokenHandler } from './user/UserTokenHandler';
/**
* Token authentication and credentials management.
*
* @public
*/
export const authServiceFactory = createServiceFactory({
service: coreServices.auth,
deps: {
config: coreServices.rootConfig,
logger: coreServices.rootLogger,
discovery: coreServices.discovery,
plugin: coreServices.pluginMetadata,
database: coreServices.database,
// Re-using the token manager makes sure that we use the same generated keys for
// development as plugins that have not yet been migrated. It's important that this
// keeps working as long as there are plugins that have not been migrated to the
// new auth services in the new backend system.
tokenManager: coreServices.tokenManager,
},
async factory({ config, discovery, plugin, tokenManager, logger, database }) {
const disableDefaultAuthPolicy =
config.getOptionalBoolean(
'backend.auth.dangerouslyDisableDefaultAuthPolicy',
) ?? false;
const keyDuration = { hours: 1 };
const keySource = await createPluginKeySource({
config,
database,
logger,
keyDuration,
});
const userTokens = UserTokenHandler.create({
discovery,
});
const pluginTokens = PluginTokenHandler.create({
ownPluginId: plugin.getId(),
logger,
keySource,
keyDuration,
discovery,
});
const externalTokens = ExternalTokenHandler.create({
ownPluginId: plugin.getId(),
config,
logger,
});
return new DefaultAuthService(
userTokens,
pluginTokens,
externalTokens,
tokenManager,
plugin.getId(),
disableDefaultAuthPolicy,
keySource,
);
},
});
@@ -0,0 +1,17 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { authServiceFactory } from './authServiceFactory';
@@ -37,8 +37,8 @@ type Row = {
export function applyDatabaseMigrations(knex: Knex): Promise<void> {
const migrationsDir = resolvePackagePath(
'@backstage/backend-app-api',
'migrations',
'@backstage/backend-defaults',
'migrations/auth',
);
return knex.migrate.latest({
@@ -0,0 +1,290 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
AuthService,
BackstageCredentials,
BackstagePrincipalTypes,
BackstageUserPrincipal,
DiscoveryService,
HttpAuthService,
coreServices,
createServiceFactory,
} from '@backstage/backend-plugin-api';
import { AuthenticationError, NotAllowedError } from '@backstage/errors';
import { parse as parseCookie } from 'cookie';
import { Request, Response } from 'express';
const FIVE_MINUTES_MS = 5 * 60 * 1000;
const BACKSTAGE_AUTH_COOKIE = 'backstage-auth';
function getTokenFromRequest(req: Request) {
// TODO: support multiple auth headers (iterate rawHeaders)
const authHeader = req.headers.authorization;
if (typeof authHeader === 'string') {
const matches = authHeader.match(/^Bearer[ ]+(\S+)$/i);
const token = matches?.[1];
if (token) {
return token;
}
}
return undefined;
}
function getCookieFromRequest(req: Request) {
const cookieHeader = req.headers.cookie;
if (cookieHeader) {
const cookies = parseCookie(cookieHeader);
const token = cookies[BACKSTAGE_AUTH_COOKIE];
if (token) {
return token;
}
}
return undefined;
}
function willExpireSoon(expiresAt: Date) {
return Date.now() + FIVE_MINUTES_MS > expiresAt.getTime();
}
const credentialsSymbol = Symbol('backstage-credentials');
const limitedCredentialsSymbol = Symbol('backstage-limited-credentials');
type RequestWithCredentials = Request & {
[credentialsSymbol]?: Promise<BackstageCredentials>;
[limitedCredentialsSymbol]?: Promise<BackstageCredentials>;
};
class DefaultHttpAuthService implements HttpAuthService {
readonly #auth: AuthService;
readonly #discovery: DiscoveryService;
readonly #pluginId: string;
constructor(
auth: AuthService,
discovery: DiscoveryService,
pluginId: string,
) {
this.#auth = auth;
this.#discovery = discovery;
this.#pluginId = pluginId;
}
async #extractCredentialsFromRequest(req: Request) {
const token = getTokenFromRequest(req);
if (!token) {
return await this.#auth.getNoneCredentials();
}
return await this.#auth.authenticate(token);
}
async #extractLimitedCredentialsFromRequest(req: Request) {
const token = getTokenFromRequest(req);
if (token) {
return await this.#auth.authenticate(token, {
allowLimitedAccess: true,
});
}
const cookie = getCookieFromRequest(req);
if (cookie) {
return await this.#auth.authenticate(cookie, {
allowLimitedAccess: true,
});
}
return await this.#auth.getNoneCredentials();
}
async #getCredentials(req: RequestWithCredentials) {
return (req[credentialsSymbol] ??=
this.#extractCredentialsFromRequest(req));
}
async #getLimitedCredentials(req: RequestWithCredentials) {
return (req[limitedCredentialsSymbol] ??=
this.#extractLimitedCredentialsFromRequest(req));
}
async credentials<TAllowed extends keyof BackstagePrincipalTypes = 'unknown'>(
req: Request,
options?: {
allow?: Array<TAllowed>;
allowLimitedAccess?: boolean;
},
): Promise<BackstageCredentials<BackstagePrincipalTypes[TAllowed]>> {
// Limited and full credentials are treated as two separate cases, this lets
// us avoid internal dependencies between the AuthService and
// HttpAuthService implementations
const credentials = options?.allowLimitedAccess
? await this.#getLimitedCredentials(req)
: await this.#getCredentials(req);
const allowed = options?.allow;
if (!allowed) {
return credentials as any;
}
if (this.#auth.isPrincipal(credentials, 'none')) {
if (allowed.includes('none' as TAllowed)) {
return credentials as any;
}
throw new AuthenticationError('Missing credentials');
} else if (this.#auth.isPrincipal(credentials, 'user')) {
if (allowed.includes('user' as TAllowed)) {
return credentials as any;
}
throw new NotAllowedError(
`This endpoint does not allow 'user' credentials`,
);
} else if (this.#auth.isPrincipal(credentials, 'service')) {
if (allowed.includes('service' as TAllowed)) {
return credentials as any;
}
throw new NotAllowedError(
`This endpoint does not allow 'service' credentials`,
);
}
throw new NotAllowedError(
'Unknown principal type, this should never happen',
);
}
async issueUserCookie(
res: Response,
options?: { credentials?: BackstageCredentials },
): Promise<{ expiresAt: Date }> {
if (res.headersSent) {
throw new Error('Failed to issue user cookie, headers were already sent');
}
let credentials: BackstageCredentials<BackstageUserPrincipal>;
if (options?.credentials) {
if (this.#auth.isPrincipal(options.credentials, 'none')) {
res.clearCookie(
BACKSTAGE_AUTH_COOKIE,
await this.#getCookieOptions(res.req),
);
return { expiresAt: new Date() };
}
if (!this.#auth.isPrincipal(options.credentials, 'user')) {
throw new AuthenticationError(
'Refused to issue cookie for non-user principal',
);
}
credentials = options.credentials;
} else {
credentials = await this.credentials(res.req, { allow: ['user'] });
}
const existingExpiresAt = await this.#existingCookieExpiration(res.req);
if (existingExpiresAt && !willExpireSoon(existingExpiresAt)) {
return { expiresAt: existingExpiresAt };
}
const { token, expiresAt } = await this.#auth.getLimitedUserToken(
credentials,
);
if (!token) {
throw new Error('User credentials is unexpectedly missing token');
}
res.cookie(BACKSTAGE_AUTH_COOKIE, token, {
...(await this.#getCookieOptions(res.req)),
expires: expiresAt,
});
return { expiresAt };
}
async #getCookieOptions(_req: Request): Promise<{
domain: string;
httpOnly: true;
secure: boolean;
priority: 'high';
sameSite: 'none' | 'lax';
}> {
// TODO: eventually we should read from `${req.protocol}://${req.hostname}`
// once https://github.com/backstage/backstage/issues/24169 has landed
const externalBaseUrlStr = await this.#discovery.getExternalBaseUrl(
this.#pluginId,
);
const externalBaseUrl = new URL(externalBaseUrlStr);
const secure =
externalBaseUrl.protocol === 'https:' ||
externalBaseUrl.hostname === 'localhost';
return {
domain: externalBaseUrl.hostname,
httpOnly: true,
secure,
priority: 'high',
sameSite: secure ? 'none' : 'lax',
};
}
async #existingCookieExpiration(req: Request): Promise<Date | undefined> {
const existingCookie = getCookieFromRequest(req);
if (!existingCookie) {
return undefined;
}
try {
const existingCredentials = await this.#auth.authenticate(
existingCookie,
{
allowLimitedAccess: true,
},
);
if (!this.#auth.isPrincipal(existingCredentials, 'user')) {
return undefined;
}
return existingCredentials.expiresAt;
} catch (error) {
if (error.name === 'AuthenticationError') {
return undefined;
}
throw error;
}
}
}
/**
* Authentication of HTTP requests.
*
* @public
*/
export const httpAuthServiceFactory = createServiceFactory({
service: coreServices.httpAuth,
deps: {
auth: coreServices.auth,
discovery: coreServices.discovery,
plugin: coreServices.pluginMetadata,
},
async factory({ auth, discovery, plugin }) {
return new DefaultHttpAuthService(auth, discovery, plugin.getId());
},
});
@@ -0,0 +1,17 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { httpAuthServiceFactory } from './httpAuthServiceFactory';
@@ -0,0 +1,17 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { userInfoServiceFactory } from './userInfoServiceFactory';
@@ -0,0 +1,103 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
UserInfoService,
BackstageUserInfo,
coreServices,
createServiceFactory,
DiscoveryService,
BackstageCredentials,
} from '@backstage/backend-plugin-api';
import { ResponseError } from '@backstage/errors';
import { decodeJwt } from 'jose';
import fetch from 'node-fetch';
import { toInternalBackstageCredentials } from '../auth/helpers';
type Options = {
discovery: DiscoveryService;
};
export class DefaultUserInfoService implements UserInfoService {
private readonly discovery: DiscoveryService;
constructor(options: Options) {
this.discovery = options.discovery;
}
async getUserInfo(
credentials: BackstageCredentials,
): Promise<BackstageUserInfo> {
const internalCredentials = toInternalBackstageCredentials(credentials);
if (internalCredentials.principal.type !== 'user') {
throw new Error('Only user credentials are supported');
}
if (!internalCredentials.token) {
throw new Error('User credentials is unexpectedly missing token');
}
const { sub: userEntityRef, ent: tokenEnt } = decodeJwt(
internalCredentials.token,
);
if (typeof userEntityRef !== 'string') {
throw new Error('User entity ref must be a string');
}
let ownershipEntityRefs = tokenEnt;
if (!ownershipEntityRefs) {
const userInfoResp = await fetch(
`${await this.discovery.getBaseUrl('auth')}/v1/userinfo`,
{
headers: {
Authorization: `Bearer ${internalCredentials.token}`,
},
},
);
if (!userInfoResp.ok) {
throw await ResponseError.fromResponse(userInfoResp);
}
const {
claims: { ent },
} = await userInfoResp.json();
ownershipEntityRefs = ent;
}
if (!ownershipEntityRefs) {
throw new Error('Ownership entity refs can not be determined');
} else if (
!Array.isArray(ownershipEntityRefs) ||
ownershipEntityRefs.some(ref => typeof ref !== 'string')
) {
throw new Error('Ownership entity refs must be an array of strings');
}
return { userEntityRef, ownershipEntityRefs };
}
}
/** @public */
export const userInfoServiceFactory = createServiceFactory({
service: coreServices.userInfo,
deps: {
discovery: coreServices.discovery,
},
async factory({ discovery }) {
return new DefaultUserInfoService({ discovery });
},
});
@@ -23,7 +23,11 @@ import { createServiceRef } from '../system';
*/
export namespace coreServices {
/**
* The service reference for the plugin scoped {@link AuthService}.
* Handles token authentication and credentials management.
*
* See {@link AuthService}
* and {@link https://backstage.io/docs/backend-system/core-services/auth | the service docs}
* for more information.
*
* @public
*/
@@ -32,7 +36,11 @@ export namespace coreServices {
});
/**
* The service reference for the plugin scoped {@link UserInfoService}.
* Authenticated user information retrieval.
*
* See {@link UserInfoService}
* and {@link https://backstage.io/docs/backend-system/core-services/user-info | the service docs}
* for more information.
*
* @public
*/
@@ -43,7 +51,11 @@ export namespace coreServices {
});
/**
* The service reference for the plugin scoped {@link CacheService}.
* Key-value store for caching data.
*
* See {@link CacheService}
* and {@link https://backstage.io/docs/backend-system/core-services/cache | the service docs}
* for more information.
*
* @public
*/
@@ -52,7 +64,11 @@ export namespace coreServices {
});
/**
* The service reference for the root scoped {@link RootConfigService}.
* Access to static configuration.
*
* See {@link RootConfigService}
* and {@link https://backstage.io/docs/backend-system/core-services/root-config | the service docs}
* for more information.
*
* @public
*/
@@ -61,7 +77,11 @@ export namespace coreServices {
>({ id: 'core.rootConfig', scope: 'root' });
/**
* The service reference for the plugin scoped {@link DatabaseService}.
* Database access and management via `knex`.
*
* See {@link DatabaseService}
* and {@link https://backstage.io/docs/backend-system/core-services/database | the service docs}
* for more information.
*
* @public
*/
@@ -70,7 +90,11 @@ export namespace coreServices {
>({ id: 'core.database' });
/**
* The service reference for the plugin scoped {@link DiscoveryService}.
* Service discovery for inter-plugin communication.
*
* See {@link DiscoveryService}
* and {@link https://backstage.io/docs/backend-system/core-services/discovery | the service docs}
* for more information.
*
* @public
*/
@@ -79,7 +103,11 @@ export namespace coreServices {
>({ id: 'core.discovery' });
/**
* The service reference for the plugin scoped {@link HttpAuthService}.
* Authentication of HTTP requests.
*
* See {@link HttpAuthService}
* and {@link https://backstage.io/docs/backend-system/core-services/http-auth | the service docs}
* for more information.
*
* @public
*/
@@ -88,7 +116,11 @@ export namespace coreServices {
>({ id: 'core.httpAuth' });
/**
* The service reference for the plugin scoped {@link HttpRouterService}.
* HTTP route registration for plugins.
*
* See {@link HttpRouterService}
* and {@link https://backstage.io/docs/backend-system/core-services/http-router | the service docs}
* for more information.
*
* @public
*/
@@ -97,7 +129,11 @@ export namespace coreServices {
>({ id: 'core.httpRouter' });
/**
* The service reference for the plugin scoped {@link LifecycleService}.
* Registration of plugin startup and shutdown lifecycle hooks.
*
* See {@link LifecycleService}
* and {@link https://backstage.io/docs/backend-system/core-services/lifecycle | the service docs}
* for more information.
*
* @public
*/
@@ -106,7 +142,11 @@ export namespace coreServices {
>({ id: 'core.lifecycle' });
/**
* The service reference for the plugin scoped {@link LoggerService}.
* Plugin-level logging.
*
* See {@link LoggerService}
* and {@link https://backstage.io/docs/backend-system/core-services/logger | the service docs}
* for more information.
*
* @public
*/
@@ -115,7 +155,11 @@ export namespace coreServices {
>({ id: 'core.logger' });
/**
* The service reference for the plugin scoped {@link PermissionsService}.
* Permission system integration for authorization of user actions.
*
* See {@link PermissionsService}
* and {@link https://backstage.io/docs/backend-system/core-services/permissions | the service docs}
* for more information.
*
* @public
*/
@@ -124,7 +168,11 @@ export namespace coreServices {
>({ id: 'core.permissions' });
/**
* The service reference for the plugin scoped {@link PluginMetadataService}.
* Built-in service for accessing metadata about the current plugin.
*
* See {@link PluginMetadataService}
* and {@link https://backstage.io/docs/backend-system/core-services/plugin-metadata | the service docs}
* for more information.
*
* @public
*/
@@ -133,7 +181,11 @@ export namespace coreServices {
>({ id: 'core.pluginMetadata' });
/**
* The service reference for the root scoped {@link RootHttpRouterService}.
* HTTP route registration for root services.
*
* See {@link RootHttpRouterService}
* and {@link https://backstage.io/docs/backend-system/core-services/root-http-router | the service docs}
* for more information.
*
* @public
*/
@@ -142,7 +194,11 @@ export namespace coreServices {
>({ id: 'core.rootHttpRouter', scope: 'root' });
/**
* The service reference for the root scoped {@link RootLifecycleService}.
* Registration of backend startup and shutdown lifecycle hooks.
*
* See {@link RootLifecycleService}
* and {@link https://backstage.io/docs/backend-system/core-services/root-lifecycle | the service docs}
* for more information.
*
* @public
*/
@@ -151,7 +207,11 @@ export namespace coreServices {
>({ id: 'core.rootLifecycle', scope: 'root' });
/**
* The service reference for the root scoped {@link RootLoggerService}.
* Root-level logging.
*
* See {@link RootLoggerService}
* and {@link https://backstage.io/docs/backend-system/core-services/root-logger | the service docs}
* for more information.
*
* @public
*/
@@ -160,7 +220,11 @@ export namespace coreServices {
>({ id: 'core.rootLogger', scope: 'root' });
/**
* The service reference for the plugin scoped {@link SchedulerService}.
* Scheduling of distributed background tasks.
*
* See {@link SchedulerService}
* and {@link https://backstage.io/docs/backend-system/core-services/scheduler | the service docs}
* for more information.
*
* @public
*/
@@ -169,7 +233,11 @@ export namespace coreServices {
>({ id: 'core.scheduler' });
/**
* The service reference for the plugin scoped {@link TokenManagerService}.
* Deprecated service authentication service, use the `auth` service instead.
*
* See {@link TokenManagerService}
* and {@link https://backstage.io/docs/backend-system/core-services/token-manager | the service docs}
* for more information.
*
* @public
* @deprecated Please migrate to the new `coreServices.auth`, `coreServices.httpAuth`, and `coreServices.userInfo` services as needed instead
@@ -179,7 +247,11 @@ export namespace coreServices {
>({ id: 'core.tokenManager' });
/**
* The service reference for the plugin scoped {@link UrlReaderService}.
* Reading content from external systems.
*
* See {@link UrlReaderService}
* and {@link https://backstage.io/docs/backend-system/core-services/url-reader | the service docs}
* for more information.
*
* @public
*/
@@ -188,7 +260,11 @@ export namespace coreServices {
>({ id: 'core.urlReader' });
/**
* The service reference for the plugin scoped {@link IdentityService}.
* Deprecated user authentication service, use the `auth` service instead.
*
* See {@link IdentityService}
* and {@link https://backstage.io/docs/backend-system/core-services/identity | the service docs}
* for more information.
*
* @public
* @deprecated Please migrate to the new `coreServices.auth`, `coreServices.httpAuth`, and `coreServices.userInfo` services as needed instead
+5
View File
@@ -3606,6 +3606,7 @@ __metadata:
"@aws-sdk/types": ^3.347.0
"@aws-sdk/util-stream-node": ^3.350.0
"@backstage/backend-app-api": "workspace:^"
"@backstage/backend-common": "workspace:^"
"@backstage/backend-dev-utils": "workspace:^"
"@backstage/backend-plugin-api": "workspace:^"
"@backstage/backend-test-utils": "workspace:^"
@@ -3615,6 +3616,7 @@ __metadata:
"@backstage/errors": "workspace:^"
"@backstage/integration": "workspace:^"
"@backstage/integration-aws-node": "workspace:^"
"@backstage/plugin-auth-node": "workspace:^"
"@backstage/plugin-events-node": "workspace:^"
"@backstage/plugin-permission-node": "workspace:^"
"@backstage/types": "workspace:^"
@@ -3628,10 +3630,13 @@ __metadata:
base64-stream: ^1.0.0
better-sqlite3: ^9.0.0
concat-stream: ^2.0.0
cookie: ^0.6.0
cron: ^3.0.0
express: ^4.17.1
fs-extra: ^11.2.0
git-url-parse: ^14.0.0
isomorphic-git: ^1.23.0
jose: ^5.0.0
keyv: ^4.5.2
knex: ^3.0.0
lodash: ^4.17.21