Merge pull request #12542 from RoadieHQ/identity-api-client-interface

Identity api client interface
This commit is contained in:
Patrik Oldsberg
2022-09-01 12:44:19 +02:00
committed by GitHub
33 changed files with 1969 additions and 680 deletions
+3
View File
@@ -14,6 +14,7 @@ import { createPullRequest } from 'octokit-plugin-create-pull-request';
import { Entity } from '@backstage/catalog-model';
import express from 'express';
import { GithubCredentialsProvider } from '@backstage/integration';
import { IdentityApi } from '@backstage/plugin-auth-node';
import { JsonObject } from '@backstage/types';
import { JsonValue } from '@backstage/types';
import { Knex } from 'knex';
@@ -535,6 +536,8 @@ export interface RouterOptions {
// (undocumented)
database: PluginDatabaseManager;
// (undocumented)
identity?: IdentityApi;
// (undocumented)
logger: Logger;
// (undocumented)
reader: UrlReader;
+1
View File
@@ -41,6 +41,7 @@
"@backstage/config": "^1.0.1",
"@backstage/errors": "^1.1.0",
"@backstage/integration": "^1.3.1-next.0",
"@backstage/plugin-auth-node": "^0.2.5-next.1",
"@backstage/plugin-catalog-backend": "^1.4.0-next.1",
"@backstage/plugin-catalog-node": "^1.0.2-next.0",
"@backstage/plugin-scaffolder-common": "^1.2.0-next.0",
File diff suppressed because it is too large Load Diff
@@ -22,7 +22,7 @@ import {
stringifyEntityRef,
UserEntity,
} from '@backstage/catalog-model';
import { Config, JsonObject } from '@backstage/config';
import { Config, JsonObject, JsonValue } from '@backstage/config';
import { InputError, NotFoundError, stringifyError } from '@backstage/errors';
import { ScmIntegrations } from '@backstage/integration';
import {
@@ -30,7 +30,6 @@ import {
TemplateEntityV1beta3,
templateEntityV1beta3Validator,
} from '@backstage/plugin-scaffolder-common';
import { JsonValue } from '@backstage/types';
import express from 'express';
import Router from 'express-promise-router';
import { validate } from 'jsonschema';
@@ -48,6 +47,10 @@ import {
import { createDryRunner } from '../scaffolder/dryrun';
import { StorageTaskBroker } from '../scaffolder/tasks/StorageTaskBroker';
import { findTemplate, getEntityBaseUrl, getWorkingDirectory } from './helpers';
import {
IdentityApi,
IdentityApiGetIdentityRequest,
} from '@backstage/plugin-auth-node';
/**
* RouterOptions
@@ -60,16 +63,81 @@ export interface RouterOptions {
reader: UrlReader;
database: PluginDatabaseManager;
catalogClient: CatalogApi;
actions?: TemplateAction<any>[];
taskWorkers?: number;
taskBroker?: TaskBroker;
additionalTemplateFilters?: Record<string, TemplateFilter>;
identity?: IdentityApi;
}
function isSupportedTemplate(entity: TemplateEntityV1beta3) {
return entity.apiVersion === 'scaffolder.backstage.io/v1beta3';
}
/*
* @deprecated This function remains as the DefaultIdentityClient behaves slightly differently to the pre-existing
* scaffolder behaviour. Specifically if the token fails to parse, the DefaultIdentityClient will raise an error.
* The scaffolder did not raise an error in this case. As such we chose to allow it to behave as it did previously
* until someone explicitly passes an IdentityApi. When we have reasonable confidence that most backstage deployments
* are using the IdentityApi, we can remove this function.
*/
function buildDefaultIdentityClient({
logger,
}: {
logger: Logger;
}): IdentityApi {
return {
getIdentity: async ({ request }: IdentityApiGetIdentityRequest) => {
const header = request.headers.authorization;
if (!header) {
return undefined;
}
try {
const token = header.match(/^Bearer\s(\S+\.\S+\.\S+)$/i)?.[1];
if (!token) {
throw new TypeError('Expected Bearer with JWT');
}
const [_header, rawPayload, _signature] = token.split('.');
const payload: JsonValue = JSON.parse(
Buffer.from(rawPayload, 'base64').toString(),
);
if (
typeof payload !== 'object' ||
payload === null ||
Array.isArray(payload)
) {
throw new TypeError('Malformed JWT payload');
}
const sub = payload.sub;
if (typeof sub !== 'string') {
throw new TypeError('Expected string sub claim');
}
// Check that it's a valid ref, otherwise this will throw.
parseEntityRef(sub);
return {
identity: {
userEntityRef: sub,
ownershipEntityRefs: [],
type: 'user',
},
token,
};
} catch (e) {
logger.error(`Invalid authorization header: ${stringifyError(e)}`);
return undefined;
}
},
};
}
/**
* A method to create a router for the scaffolder backend plugin.
* @public
@@ -92,6 +160,10 @@ export async function createRouter(
} = options;
const logger = parentLogger.child({ plugin: 'scaffolder' });
const identity: IdentityApi =
options.identity || buildDefaultIdentityClient({ logger });
const workingDirectory = await getWorkingDirectory(config, logger);
const integrations = ScmIntegrations.fromConfig(config);
let taskBroker: TaskBroker;
@@ -144,10 +216,12 @@ export async function createRouter(
'/v2/templates/:namespace/:kind/:name/parameter-schema',
async (req, res) => {
const { namespace, kind, name } = req.params;
const { token } = parseBearerToken({
header: req.headers.authorization,
logger,
const userIdentity = await identity.getIdentity({
request: req,
});
const token = userIdentity?.token;
const template = await findTemplate({
catalogApi: catalogClient,
entityRef: { kind, namespace, name },
@@ -188,10 +262,12 @@ export async function createRouter(
const { kind, namespace, name } = parseEntityRef(templateRef, {
defaultKind: 'template',
});
const { token, entityRef: userEntityRef } = parseBearerToken({
header: req.headers.authorization,
logger,
const callerIdentity = await identity.getIdentity({
request: req,
});
const token = callerIdentity?.token;
const userEntityRef = callerIdentity?.identity.userEntityRef;
const userEntity = userEntityRef
? await catalogClient.getEntityByRef(userEntityRef, { token })
@@ -394,10 +470,11 @@ export async function createRouter(
throw new InputError('Input template is not a template');
}
const { token } = parseBearerToken({
header: req.headers.authorization,
logger,
});
const token = (
await identity.getIdentity({
request: req,
})
)?.token;
for (const parameters of [template.spec.parameters ?? []].flat()) {
const result = validate(body.values, parameters);
@@ -447,51 +524,3 @@ export async function createRouter(
return app;
}
function parseBearerToken({
header,
logger,
}: {
header?: string;
logger: Logger;
}): {
token?: string;
entityRef?: string;
} {
if (!header) {
return {};
}
try {
const token = header.match(/^Bearer\s(\S+\.\S+\.\S+)$/i)?.[1];
if (!token) {
throw new TypeError('Expected Bearer with JWT');
}
const [_header, rawPayload, _signature] = token.split('.');
const payload: JsonValue = JSON.parse(
Buffer.from(rawPayload, 'base64').toString(),
);
if (
typeof payload !== 'object' ||
payload === null ||
Array.isArray(payload)
) {
throw new TypeError('Malformed JWT payload');
}
const sub = payload.sub;
if (typeof sub !== 'string') {
throw new TypeError('Expected string sub claim');
}
// Check that it's a valid ref, otherwise this will throw.
parseEntityRef(sub);
return { entityRef: sub, token };
} catch (e) {
logger.error(`Invalid authorization header: ${stringifyError(e)}`);
return {};
}
}