bug: fixing the passing of backstage credentials everywhere

Signed-off-by: blam <ben@blam.sh>
This commit is contained in:
blam
2024-04-05 11:14:30 +02:00
parent 7882ea60d3
commit 022f1953cb
10 changed files with 272 additions and 148 deletions
@@ -63,10 +63,13 @@ export const addResourceRoutesToRouter = (
router.post('/resources/workloads/query', async (req, res) => {
const entity = await getEntityByReq(req);
const response = await objectsProvider.getKubernetesObjectsByEntity({
entity,
auth: req.body.auth,
});
const response = await objectsProvider.getKubernetesObjectsByEntity(
{
entity,
auth: req.body.auth,
},
{ credentials: await httpAuth.credentials(req) },
);
res.json(response);
});
@@ -81,11 +84,14 @@ export const addResourceRoutesToRouter = (
throw new InputError('at least 1 customResource is required');
}
const response = await objectsProvider.getCustomResourcesByEntity({
entity,
customResources: req.body.customResources,
auth: req.body.auth,
});
const response = await objectsProvider.getCustomResourcesByEntity(
{
entity,
customResources: req.body.customResources,
auth: req.body.auth,
},
{ credentials: await httpAuth.credentials(req) },
);
res.json(response);
});
};
@@ -33,7 +33,7 @@ export class CatalogRelationServiceLocator implements KubernetesServiceLocator {
// As this implementation always returns all clusters serviceId is ignored here
getClustersByEntity(
entity: Entity,
_requestContext: ServiceLocatorRequestContext,
requestContext: ServiceLocatorRequestContext,
): Promise<{ clusters: ClusterDetails[] }> {
if (
entity.relations &&
@@ -41,13 +41,15 @@ export class CatalogRelationServiceLocator implements KubernetesServiceLocator {
r => r.type === 'dependsOn' && r.targetRef.includes('resource:'),
)
) {
return this.clusterSupplier.getClusters().then(clusters => {
return {
clusters: clusters.filter(c =>
this.doesEntityDependOnCluster(entity, c),
),
};
});
return this.clusterSupplier
.getClusters({ credentials: requestContext.credentials })
.then(clusters => {
return {
clusters: clusters.filter(c =>
this.doesEntityDependOnCluster(entity, c),
),
};
});
}
return Promise.resolve({ clusters: [] });
}
@@ -34,8 +34,10 @@ export class MultiTenantServiceLocator implements KubernetesServiceLocator {
// As this implementation always returns all clusters serviceId is ignored here
getClustersByEntity(
_entity: Entity,
_requestContext: ServiceLocatorRequestContext,
requestContext: ServiceLocatorRequestContext,
): Promise<{ clusters: ClusterDetails[] }> {
return this.clusterSupplier.getClusters().then(clusters => ({ clusters }));
return this.clusterSupplier
.getClusters({ credentials: requestContext.credentials })
.then(clusters => ({ clusters }));
}
}
@@ -34,21 +34,25 @@ export class SingleTenantServiceLocator implements KubernetesServiceLocator {
// As this implementation always returns all clusters serviceId is ignored here
getClustersByEntity(
_entity: Entity,
_requestContext: ServiceLocatorRequestContext,
requestContext: ServiceLocatorRequestContext,
): Promise<{ clusters: ClusterDetails[] }> {
return this.clusterSupplier.getClusters().then(clusters => {
if (_entity.metadata?.annotations?.['backstage.io/kubernetes-cluster']) {
return {
clusters: clusters.filter(
c =>
c.name ===
_entity.metadata?.annotations?.[
'backstage.io/kubernetes-cluster'
],
),
};
}
return { clusters };
});
return this.clusterSupplier
.getClusters({ credentials: requestContext.credentials })
.then(clusters => {
if (
_entity.metadata?.annotations?.['backstage.io/kubernetes-cluster']
) {
return {
clusters: clusters.filter(
c =>
c.name ===
_entity.metadata?.annotations?.[
'backstage.io/kubernetes-cluster'
],
),
};
}
return { clusters };
});
}
}
@@ -157,7 +157,7 @@ export class KubernetesBuilder {
const authStrategyMap = this.getAuthStrategyMap();
const proxy = this.getProxy(logger, clusterSupplier);
const proxy = this.getProxy(logger, clusterSupplier, httpAuth);
const serviceLocator = this.getServiceLocator();
@@ -350,6 +350,7 @@ export class KubernetesBuilder {
protected buildProxy(
logger: Logger,
clusterSupplier: KubernetesClustersSupplier,
httpAuth: HttpAuthService,
): KubernetesProxy {
const authStrategyMap = this.getAuthStrategyMap();
const authStrategy = new DispatchStrategy({
@@ -359,6 +360,7 @@ export class KubernetesBuilder {
logger,
clusterSupplier,
authStrategy,
httpAuth,
});
return this.proxy;
}
@@ -386,10 +388,13 @@ export class KubernetesBuilder {
const serviceId = req.params.serviceId;
const requestBody: ObjectsByEntityRequest = req.body;
try {
const response = await objectsProvider.getKubernetesObjectsByEntity({
entity: requestBody.entity,
auth: requestBody.auth || {},
});
const response = await objectsProvider.getKubernetesObjectsByEntity(
{
entity: requestBody.entity,
auth: requestBody.auth || {},
},
{ credentials: await httpAuth.credentials(req) },
);
res.json(response);
} catch (e) {
logger.error(
@@ -530,8 +535,9 @@ export class KubernetesBuilder {
protected getProxy(
logger: Logger,
clusterSupplier: KubernetesClustersSupplier,
httpAuth: HttpAuthService,
) {
return this.proxy ?? this.buildProxy(logger, clusterSupplier);
return this.proxy ?? this.buildProxy(logger, clusterSupplier, httpAuth);
}
protected getAuthStrategyMap() {
@@ -35,6 +35,7 @@ import {
} from '@backstage/plugin-kubernetes-common';
import { Config, ConfigReader } from '@backstage/config';
import { Entity } from '@backstage/catalog-model';
import { BackstageCredentials } from '@backstage/backend-plugin-api';
describe('KubernetesFanOutHandler', () => {
const fetchObjectsForService = jest.fn();
@@ -62,6 +63,14 @@ describe('KubernetesFanOutHandler', () => {
pod: {},
};
const mockCredentials: BackstageCredentials = {
$$type: '@backstage/BackstageCredentials',
principal: {
userEntityRef: 'user:default/guest',
type: 'user',
},
};
const entity = {
apiVersion: 'backstage.io/v1beta1',
kind: 'Component',
@@ -372,10 +381,13 @@ describe('KubernetesFanOutHandler', () => {
sut = getKubernetesFanOutHandler([]);
const result = await sut.getKubernetesObjectsByEntity({
entity,
auth: {},
});
const result = await sut.getKubernetesObjectsByEntity(
{
entity,
auth: {},
},
{ credentials: mockCredentials },
);
expect(getClustersByEntity).toHaveBeenCalledTimes(1);
expect(fetchObjectsForService).toHaveBeenCalledTimes(1);
@@ -410,10 +422,13 @@ describe('KubernetesFanOutHandler', () => {
sut = getKubernetesFanOutHandler([]);
await sut.getKubernetesObjectsByEntity({
entity,
auth: {},
});
await sut.getKubernetesObjectsByEntity(
{
entity,
auth: {},
},
{ credentials: mockCredentials },
);
expect(fetchObjectsForService).toHaveBeenCalledTimes(1);
expect(
@@ -442,10 +457,13 @@ describe('KubernetesFanOutHandler', () => {
sut = getKubernetesFanOutHandler([]);
await sut.getKubernetesObjectsByEntity({
entity,
auth: {},
});
await sut.getKubernetesObjectsByEntity(
{
entity,
auth: {},
},
{ credentials: mockCredentials },
);
expect(fetchObjectsForService).toHaveBeenCalledTimes(2);
expect(fetchObjectsForService).toHaveBeenCalledWith(
@@ -497,10 +515,13 @@ describe('KubernetesFanOutHandler', () => {
},
]);
await sut.getKubernetesObjectsByEntity({
entity,
auth: {},
});
await sut.getKubernetesObjectsByEntity(
{
entity,
auth: {},
},
{ credentials: mockCredentials },
);
expect(fetchObjectsForService).toHaveBeenCalledTimes(2);
expect(fetchObjectsForService).toHaveBeenCalledWith(
@@ -559,10 +580,13 @@ describe('KubernetesFanOutHandler', () => {
},
]);
await sut.getKubernetesObjectsByEntity({
entity,
auth: {},
});
await sut.getKubernetesObjectsByEntity(
{
entity,
auth: {},
},
{ credentials: mockCredentials },
);
expect(fetchObjectsForService).toHaveBeenCalledWith(
expect.objectContaining({
@@ -600,10 +624,13 @@ describe('KubernetesFanOutHandler', () => {
},
]);
await sut.getKubernetesObjectsByEntity({
entity,
auth: {},
});
await sut.getKubernetesObjectsByEntity(
{
entity,
auth: {},
},
{ credentials: mockCredentials },
);
expect(fetchObjectsForService).toHaveBeenCalledWith(
expect.objectContaining({
@@ -665,10 +692,13 @@ describe('KubernetesFanOutHandler', () => {
sut = getKubernetesFanOutHandler([]);
const result = await sut.getKubernetesObjectsByEntity({
entity,
auth: {},
});
const result = await sut.getKubernetesObjectsByEntity(
{
entity,
auth: {},
},
{ credentials: mockCredentials },
);
expect(getClustersByEntity).toHaveBeenCalledTimes(1);
expect(fetchObjectsForService).toHaveBeenCalledTimes(1);
@@ -744,10 +774,13 @@ describe('KubernetesFanOutHandler', () => {
sut = getKubernetesFanOutHandler([]);
const result = await sut.getKubernetesObjectsByEntity({
entity,
auth: {},
});
const result = await sut.getKubernetesObjectsByEntity(
{
entity,
auth: {},
},
{ credentials: mockCredentials },
);
expect(getClustersByEntity).toHaveBeenCalledTimes(1);
expect(fetchObjectsForService).toHaveBeenCalledTimes(1);
@@ -779,12 +812,15 @@ describe('KubernetesFanOutHandler', () => {
sut = getKubernetesFanOutHandler([]);
const result = await sut.getKubernetesObjectsByEntity({
entity,
auth: {
google: 'google_token_123',
const result = await sut.getKubernetesObjectsByEntity(
{
entity,
auth: {
google: 'google_token_123',
},
},
});
{ credentials: mockCredentials },
);
expect(getClustersByEntity).toHaveBeenCalledTimes(1);
expect(fetchObjectsForService).toHaveBeenCalledTimes(2);
@@ -836,12 +872,15 @@ describe('KubernetesFanOutHandler', () => {
sut = getKubernetesFanOutHandler([]);
const result = await sut.getKubernetesObjectsByEntity({
entity,
auth: {
google: 'google_token_123',
const result = await sut.getKubernetesObjectsByEntity(
{
entity,
auth: {
google: 'google_token_123',
},
},
});
{ credentials: mockCredentials },
);
expect(getClustersByEntity).toHaveBeenCalledTimes(1);
expect(fetchObjectsForService).toHaveBeenCalledTimes(3);
@@ -897,12 +936,15 @@ describe('KubernetesFanOutHandler', () => {
sut = getKubernetesFanOutHandler([]);
const result = await sut.getKubernetesObjectsByEntity({
entity,
auth: {
google: 'google_token_123',
const result = await sut.getKubernetesObjectsByEntity(
{
entity,
auth: {
google: 'google_token_123',
},
},
});
{ credentials: mockCredentials },
);
expect(getClustersByEntity).toHaveBeenCalledTimes(1);
expect(fetchObjectsForService).toHaveBeenCalledTimes(4);
@@ -1015,12 +1057,15 @@ describe('KubernetesFanOutHandler', () => {
sut = getKubernetesFanOutHandler([]);
const result = await sut.getKubernetesObjectsByEntity({
entity,
auth: {
google: 'google_token_123',
const result = await sut.getKubernetesObjectsByEntity(
{
entity,
auth: {
google: 'google_token_123',
},
},
});
{ credentials: mockCredentials },
);
expect(getClustersByEntity).toHaveBeenCalledTimes(1);
expect(fetchObjectsForService).toHaveBeenCalledTimes(2);
@@ -1069,10 +1114,13 @@ describe('KubernetesFanOutHandler', () => {
sut = getKubernetesFanOutHandler([]);
const result = sut.getKubernetesObjectsByEntity({
entity,
auth: {},
});
const result = sut.getKubernetesObjectsByEntity(
{
entity,
auth: {},
},
{ credentials: mockCredentials },
);
await expect(result).rejects.toThrow(nonFetchError);
});
@@ -1155,10 +1203,13 @@ describe('KubernetesFanOutHandler', () => {
});
const result =
await kubernetesFanOutHandler.getKubernetesObjectsByEntity({
entity,
auth: {},
});
await kubernetesFanOutHandler.getKubernetesObjectsByEntity(
{
entity,
auth: {},
},
{ credentials: mockCredentials },
);
const expected: ObjectsByEntityResponse = {
items: [
@@ -1200,17 +1251,20 @@ describe('KubernetesFanOutHandler', () => {
sut = getKubernetesFanOutHandler([]);
await sut.getCustomResourcesByEntity({
entity,
auth: {},
customResources: [
{
group: 'parameter-crd.example.com',
apiVersion: 'v1alpha1',
plural: 'parameter-crd',
},
],
});
await sut.getCustomResourcesByEntity(
{
entity,
auth: {},
customResources: [
{
group: 'parameter-crd.example.com',
apiVersion: 'v1alpha1',
plural: 'parameter-crd',
},
],
},
{ credentials: mockCredentials },
);
expect(fetchObjectsForService).toHaveBeenCalledTimes(1);
expect(fetchObjectsForService).toHaveBeenCalledWith(
@@ -1262,17 +1316,20 @@ describe('KubernetesFanOutHandler', () => {
},
]);
await sut.getCustomResourcesByEntity({
entity,
auth: {},
customResources: [
{
group: 'parameter-crd.example.com',
apiVersion: 'v1alpha1',
plural: 'parameter-crd',
},
],
});
await sut.getCustomResourcesByEntity(
{
entity,
auth: {},
customResources: [
{
group: 'parameter-crd.example.com',
apiVersion: 'v1alpha1',
plural: 'parameter-crd',
},
],
},
{ credentials: mockCredentials },
);
expect(fetchObjectsForService).toHaveBeenCalledTimes(2);
expect(fetchObjectsForService).toHaveBeenCalledWith(
@@ -1329,17 +1386,20 @@ describe('KubernetesFanOutHandler', () => {
sut = getKubernetesFanOutHandler([]);
await sut.getCustomResourcesByEntity({
entity,
auth: {},
customResources: [
{
group: 'parameter-crd.example.com',
apiVersion: 'v1alpha1',
plural: 'parameter-crd',
},
],
});
await sut.getCustomResourcesByEntity(
{
entity,
auth: {},
customResources: [
{
group: 'parameter-crd.example.com',
apiVersion: 'v1alpha1',
plural: 'parameter-crd',
},
],
},
{ credentials: mockCredentials },
);
expect(fetchObjectsForService).toHaveBeenCalledWith(
expect.objectContaining({
@@ -48,7 +48,9 @@ import {
CustomResourcesByEntity,
KubernetesCredential,
KubernetesObjectsByEntity,
KubernetesObjectsProvider,
} from '@backstage/plugin-kubernetes-node';
import { BackstageCredentials } from '@backstage/backend-plugin-api';
/**
*
@@ -190,7 +192,7 @@ const toClientSafePodMetrics = (
type responseWithMetrics = [FetchResponseWrapper, PodStatusFetchResponse[]];
export class KubernetesFanOutHandler {
export class KubernetesFanOutHandler implements KubernetesObjectsProvider {
private readonly logger: Logger;
private readonly fetcher: KubernetesFetcher;
private readonly serviceLocator: KubernetesServiceLocator;
@@ -214,30 +216,38 @@ export class KubernetesFanOutHandler {
this.authStrategy = authStrategy;
}
async getCustomResourcesByEntity({
entity,
auth,
customResources,
}: CustomResourcesByEntity): Promise<ObjectsByEntityResponse> {
async getCustomResourcesByEntity(
{ entity, auth, customResources }: CustomResourcesByEntity,
options: { credentials: BackstageCredentials },
): Promise<ObjectsByEntityResponse> {
// Don't fetch the default object types only the provided custom resources
return this.fanOutRequests(
entity,
auth,
{ credentials: options.credentials },
new Set<ObjectToFetch>(),
customResources,
);
}
async getKubernetesObjectsByEntity({
entity,
auth,
}: KubernetesObjectsByEntity): Promise<ObjectsByEntityResponse> {
return this.fanOutRequests(entity, auth, this.objectTypesToFetch);
async getKubernetesObjectsByEntity(
{ entity, auth }: KubernetesObjectsByEntity,
options: { credentials: BackstageCredentials },
): Promise<ObjectsByEntityResponse> {
return this.fanOutRequests(
entity,
auth,
{
credentials: options?.credentials,
},
this.objectTypesToFetch,
);
}
private async fanOutRequests(
entity: Entity,
auth: KubernetesRequestAuth,
options: { credentials: BackstageCredentials },
objectTypesToFetch: Set<ObjectToFetch>,
customResources?: CustomResourceMatcher[],
) {
@@ -248,6 +258,7 @@ export class KubernetesFanOutHandler {
const { clusters } = await this.serviceLocator.getClustersByEntity(entity, {
objectTypesToFetch: objectTypesToFetch,
customResources: customResources ?? [],
credentials: options.credentials,
});
this.logger.info(
@@ -55,6 +55,10 @@ import {
} from './KubernetesProxy';
import type { Request } from 'express';
import {
BackstageCredentials,
HttpAuthService,
} from '@backstage/backend-plugin-api';
const mockCertDir = createMockDirectory({
content: {
@@ -69,7 +73,10 @@ describe('KubernetesProxy', () => {
const logger = getVoidLogger();
const clusterSupplier: jest.Mocked<KubernetesClustersSupplier> = {
getClusters: jest.fn<Promise<ClusterDetails[]>, []>(),
getClusters: jest.fn<
Promise<ClusterDetails[]>,
[{ credentials: BackstageCredentials }]
>(),
};
const permissionApi: jest.Mocked<PermissionEvaluator> = {
@@ -77,6 +84,11 @@ describe('KubernetesProxy', () => {
authorizeConditional: jest.fn(),
};
const mockHttpAuth: jest.Mocked<HttpAuthService> = {
credentials: jest.fn(),
issueUserCookie: jest.fn(),
};
setupRequestMockHandlers(worker);
const buildMockRequest = (clusterName: any, path: string): Request => {
@@ -146,7 +158,12 @@ describe('KubernetesProxy', () => {
validateCluster: jest.fn(),
presentAuthMetadata: jest.fn(),
};
proxy = new KubernetesProxy({ logger, clusterSupplier, authStrategy });
proxy = new KubernetesProxy({
logger,
clusterSupplier,
authStrategy,
httpAuth: mockHttpAuth,
});
permissionApi.authorize.mockResolvedValue([
{ result: AuthorizeResult.ALLOW },
]);
@@ -535,6 +552,7 @@ describe('KubernetesProxy', () => {
logger: getVoidLogger(),
clusterSupplier: clusterSupplier,
authStrategy: strategy,
httpAuth: mockHttpAuth,
});
worker.use(
@@ -656,6 +674,7 @@ describe('KubernetesProxy', () => {
logger: getVoidLogger(),
clusterSupplier: new LocalKubectlProxyClusterLocator(),
authStrategy: new AnonymousStrategy(),
httpAuth: mockHttpAuth,
});
worker.use(
@@ -45,6 +45,7 @@ import { ClusterDetails, KubernetesClustersSupplier } from '../types/types';
import type { Request } from 'express';
import { IncomingHttpHeaders } from 'http';
import { HttpAuthService } from '@backstage/backend-plugin-api';
export const APPLICATION_JSON: string = 'application/json';
@@ -81,6 +82,7 @@ export type KubernetesProxyOptions = {
logger: Logger;
clusterSupplier: KubernetesClustersSupplier;
authStrategy: AuthenticationStrategy;
httpAuth: HttpAuthService;
};
/**
@@ -93,11 +95,13 @@ export class KubernetesProxy {
private readonly logger: Logger;
private readonly clusterSupplier: KubernetesClustersSupplier;
private readonly authStrategy: AuthenticationStrategy;
private readonly httpAuth: HttpAuthService;
constructor(options: KubernetesProxyOptions) {
this.logger = options.logger;
this.clusterSupplier = options.clusterSupplier;
this.authStrategy = options.authStrategy;
this.httpAuth = options.httpAuth;
}
public createRequestHandler(
@@ -108,6 +112,7 @@ export class KubernetesProxy {
const authorizeResponse = await permissionApi.authorize(
[{ permission: kubernetesProxyPermission }],
{
// todo: this should be updated too.
token: getBearerTokenFromAuthorizationHeader(
req.header('authorization'),
),
@@ -220,7 +225,9 @@ export class KubernetesProxy {
private async getClusterForRequest(req: Request): Promise<ClusterDetails> {
const clusterName = req.headers[HEADER_KUBERNETES_CLUSTER.toLowerCase()];
const clusters = await this.clusterSupplier.getClusters();
const clusters = await this.clusterSupplier.getClusters({
credentials: await this.httpAuth.credentials(req),
});
if (!clusters || clusters.length <= 0) {
throw new NotFoundError(`No Clusters configured`);
+8 -1
View File
@@ -32,9 +32,15 @@ import { JsonObject } from '@backstage/types';
export interface KubernetesObjectsProvider {
getKubernetesObjectsByEntity(
kubernetesObjectsByEntity: KubernetesObjectsByEntity,
options: {
credentials: BackstageCredentials;
},
): Promise<ObjectsByEntityResponse>;
getCustomResourcesByEntity(
customResourcesByEntity: CustomResourcesByEntity,
options: {
credentials: BackstageCredentials;
},
): Promise<ObjectsByEntityResponse>;
}
@@ -135,7 +141,7 @@ export interface KubernetesClustersSupplier {
* Implementations _should_ cache the clusters and refresh them periodically,
* as getClusters is called whenever the list of clusters is needed.
*/
getClusters(options?: {
getClusters(options: {
credentials: BackstageCredentials;
}): Promise<ClusterDetails[]>;
}
@@ -248,6 +254,7 @@ export interface KubernetesFetcher {
export interface ServiceLocatorRequestContext {
objectTypesToFetch: Set<ObjectToFetch>;
customResources: CustomResourceMatcher[];
credentials: BackstageCredentials;
}
/**