diff --git a/.changeset/kind-clouds-fly.md b/.changeset/kind-clouds-fly.md new file mode 100644 index 0000000000..22aa7cda33 --- /dev/null +++ b/.changeset/kind-clouds-fly.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-kubernetes-backend': patch +'@backstage/plugin-kubernetes-node': patch +--- + +Enabled a way to include custom auth metadata info on the clusters endpoint. If you want to implement a Kubernetes auth strategy which requires surfacing custom auth metadata to the frontend, use the new presentAuthMetadata method on the AuthenticationStrategy interface. diff --git a/plugins/kubernetes-backend/api-report.md b/plugins/kubernetes-backend/api-report.md index e6996484dd..afdb3bcf9a 100644 --- a/plugins/kubernetes-backend/api-report.md +++ b/plugins/kubernetes-backend/api-report.md @@ -4,6 +4,7 @@ ```ts import { AuthenticationStrategy as AuthenticationStrategy_2 } from '@backstage/plugin-kubernetes-node'; +import { AuthMetadata as AuthMetadata_2 } from '@backstage/plugin-kubernetes-node'; import { CatalogApi } from '@backstage/catalog-client'; import { ClusterDetails as ClusterDetails_2 } from '@backstage/plugin-kubernetes-node'; import { Config } from '@backstage/config'; @@ -12,9 +13,12 @@ import { Duration } from 'luxon'; import express from 'express'; import * as k8sAuthTypes from '@backstage/plugin-kubernetes-node'; import { KubernetesClustersSupplier as KubernetesClustersSupplier_2 } from '@backstage/plugin-kubernetes-node'; +import { KubernetesCredential as KubernetesCredential_2 } from '@backstage/plugin-kubernetes-node'; +import { KubernetesFetcher as KubernetesFetcher_2 } from '@backstage/plugin-kubernetes-node'; import { KubernetesObjectsProvider as KubernetesObjectsProvider_2 } from '@backstage/plugin-kubernetes-node'; import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common'; import type { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common'; +import { KubernetesServiceLocator as KubernetesServiceLocator_2 } from '@backstage/plugin-kubernetes-node'; import { Logger } from 'winston'; import { ObjectToFetch as ObjectToFetch_2 } from '@backstage/plugin-kubernetes-node'; import { PermissionEvaluator } from '@backstage/plugin-permission-common'; @@ -23,20 +27,24 @@ import { RequestHandler } from 'http-proxy-middleware'; import { TokenCredential } from '@azure/identity'; // @public (undocumented) -export class AksStrategy implements AuthenticationStrategy { +export class AksStrategy implements AuthenticationStrategy_2 { // (undocumented) getCredential( - _: ClusterDetails, + _: ClusterDetails_2, requestAuth: KubernetesRequestAuth, - ): Promise; + ): Promise; + // (undocumented) + presentAuthMetadata(_authMetadata: AuthMetadata_2): AuthMetadata_2; // (undocumented) validateCluster(): Error[]; } // @public (undocumented) -export class AnonymousStrategy implements AuthenticationStrategy { +export class AnonymousStrategy implements AuthenticationStrategy_2 { // (undocumented) - getCredential(): Promise; + getCredential(): Promise; + // (undocumented) + presentAuthMetadata(_authMetadata: AuthMetadata_2): AuthMetadata_2; // (undocumented) validateCluster(): Error[]; } @@ -48,19 +56,25 @@ export type AuthenticationStrategy = k8sAuthTypes.AuthenticationStrategy; export type AuthMetadata = k8sAuthTypes.AuthMetadata; // @public (undocumented) -export class AwsIamStrategy implements AuthenticationStrategy { +export class AwsIamStrategy implements AuthenticationStrategy_2 { constructor(opts: { config: Config }); // (undocumented) - getCredential(clusterDetails: ClusterDetails): Promise; + getCredential( + clusterDetails: ClusterDetails_2, + ): Promise; + // (undocumented) + presentAuthMetadata(_authMetadata: AuthMetadata_2): AuthMetadata_2; // (undocumented) validateCluster(): Error[]; } // @public (undocumented) -export class AzureIdentityStrategy implements AuthenticationStrategy { +export class AzureIdentityStrategy implements AuthenticationStrategy_2 { constructor(logger: Logger, tokenCredential?: TokenCredential); // (undocumented) - getCredential(): Promise; + getCredential(): Promise; + // (undocumented) + presentAuthMetadata(_authMetadata: AuthMetadata_2): AuthMetadata_2; // (undocumented) validateCluster(): Error[]; } @@ -81,21 +95,23 @@ export type CustomResourcesByEntity = k8sAuthTypes.CustomResourcesByEntity; export const DEFAULT_OBJECTS: ObjectToFetch[]; // @public -export class DispatchStrategy implements AuthenticationStrategy { +export class DispatchStrategy implements AuthenticationStrategy_2 { constructor(options: DispatchStrategyOptions); // (undocumented) getCredential( - clusterDetails: ClusterDetails, + clusterDetails: ClusterDetails_2, auth: KubernetesRequestAuth, - ): Promise; + ): Promise; // (undocumented) - validateCluster(authMetadata: AuthMetadata): Error[]; + presentAuthMetadata(_authMetadata: AuthMetadata_2): AuthMetadata_2; + // (undocumented) + validateCluster(authMetadata: AuthMetadata_2): Error[]; } // @public (undocumented) export type DispatchStrategyOptions = { authStrategyMap: { - [key: string]: AuthenticationStrategy; + [key: string]: AuthenticationStrategy_2; }; }; @@ -103,20 +119,24 @@ export type DispatchStrategyOptions = { export type FetchResponseWrapper = k8sAuthTypes.FetchResponseWrapper; // @public (undocumented) -export class GoogleServiceAccountStrategy implements AuthenticationStrategy { +export class GoogleServiceAccountStrategy implements AuthenticationStrategy_2 { // (undocumented) - getCredential(): Promise; + getCredential(): Promise; + // (undocumented) + presentAuthMetadata(_authMetadata: AuthMetadata_2): AuthMetadata_2; // (undocumented) validateCluster(): Error[]; } // @public (undocumented) -export class GoogleStrategy implements AuthenticationStrategy { +export class GoogleStrategy implements AuthenticationStrategy_2 { // (undocumented) getCredential( - _: ClusterDetails, + _: ClusterDetails_2, requestAuth: KubernetesRequestAuth, - ): Promise; + ): Promise; + // (undocumented) + presentAuthMetadata(_authMetadata: AuthMetadata_2): AuthMetadata_2; // (undocumented) validateCluster(): Error[]; } @@ -131,7 +151,7 @@ export const HEADER_KUBERNETES_CLUSTER: string; export class KubernetesBuilder { constructor(env: KubernetesEnvironment); // (undocumented) - addAuthStrategy(key: string, strategy: AuthenticationStrategy): this; + addAuthStrategy(key: string, strategy: AuthenticationStrategy_2): this; // (undocumented) build(): KubernetesBuilderReturn; // (undocumented) @@ -145,15 +165,15 @@ export class KubernetesBuilder { // (undocumented) protected buildCustomResources(): CustomResource_2[]; // (undocumented) - protected buildFetcher(): KubernetesFetcher; + protected buildFetcher(): KubernetesFetcher_2; // (undocumented) protected buildHttpServiceLocator( _clusterSupplier: KubernetesClustersSupplier_2, - ): KubernetesServiceLocator; + ): KubernetesServiceLocator_2; // (undocumented) protected buildMultiTenantServiceLocator( clusterSupplier: KubernetesClustersSupplier_2, - ): KubernetesServiceLocator; + ): KubernetesServiceLocator_2; // (undocumented) protected buildObjectsProvider( options: KubernetesObjectsProviderOptions, @@ -175,11 +195,11 @@ export class KubernetesBuilder { protected buildServiceLocator( method: ServiceLocatorMethod, clusterSupplier: KubernetesClustersSupplier_2, - ): KubernetesServiceLocator; + ): KubernetesServiceLocator_2; // (undocumented) protected buildSingleTenantServiceLocator( clusterSupplier: KubernetesClustersSupplier_2, - ): KubernetesServiceLocator; + ): KubernetesServiceLocator_2; // (undocumented) static createBuilder(env: KubernetesEnvironment): KubernetesBuilder; // (undocumented) @@ -195,7 +215,7 @@ export class KubernetesBuilder { // (undocumented) protected getClusterSupplier(): KubernetesClustersSupplier_2; // (undocumented) - protected getFetcher(): KubernetesFetcher; + protected getFetcher(): KubernetesFetcher_2; // (undocumented) protected getObjectsProvider( options: KubernetesObjectsProviderOptions, @@ -208,38 +228,38 @@ export class KubernetesBuilder { clusterSupplier: KubernetesClustersSupplier_2, ): KubernetesProxy; // (undocumented) - protected getServiceLocator(): KubernetesServiceLocator; + protected getServiceLocator(): KubernetesServiceLocator_2; // (undocumented) protected getServiceLocatorMethod(): ServiceLocatorMethod; // (undocumented) setAuthStrategyMap(authStrategyMap: { - [key: string]: AuthenticationStrategy; + [key: string]: AuthenticationStrategy_2; }): void; // (undocumented) setClusterSupplier(clusterSupplier?: KubernetesClustersSupplier_2): this; // (undocumented) setDefaultClusterRefreshInterval(refreshInterval: Duration): this; // (undocumented) - setFetcher(fetcher?: KubernetesFetcher): this; + setFetcher(fetcher?: KubernetesFetcher_2): this; // (undocumented) setObjectsProvider(objectsProvider?: KubernetesObjectsProvider_2): this; // (undocumented) setProxy(proxy?: KubernetesProxy): this; // (undocumented) - setServiceLocator(serviceLocator?: KubernetesServiceLocator): this; + setServiceLocator(serviceLocator?: KubernetesServiceLocator_2): this; } // @public export type KubernetesBuilderReturn = Promise<{ router: express.Router; clusterSupplier: KubernetesClustersSupplier_2; - customResources: CustomResource[]; - fetcher: KubernetesFetcher; + customResources: CustomResource_2[]; + fetcher: KubernetesFetcher_2; proxy: KubernetesProxy; objectsProvider: KubernetesObjectsProvider_2; - serviceLocator: KubernetesServiceLocator; + serviceLocator: KubernetesServiceLocator_2; authStrategyMap: { - [key: string]: AuthenticationStrategy; + [key: string]: AuthenticationStrategy_2; }; }>; @@ -321,14 +341,16 @@ export type ObjectsByEntityRequest = KubernetesRequestBody; export type ObjectToFetch = k8sAuthTypes.ObjectToFetch; // @public (undocumented) -export class OidcStrategy implements AuthenticationStrategy { +export class OidcStrategy implements AuthenticationStrategy_2 { // (undocumented) getCredential( - clusterDetails: ClusterDetails, + clusterDetails: ClusterDetails_2, authConfig: KubernetesRequestAuth, - ): Promise; + ): Promise; // (undocumented) - validateCluster(authMetadata: AuthMetadata): Error[]; + presentAuthMetadata(_authMetadata: AuthMetadata_2): AuthMetadata_2; + // (undocumented) + validateCluster(authMetadata: AuthMetadata_2): Error[]; } // @public (undocumented) @@ -348,9 +370,13 @@ export interface RouterOptions { } // @public (undocumented) -export class ServiceAccountStrategy implements AuthenticationStrategy { +export class ServiceAccountStrategy implements AuthenticationStrategy_2 { // (undocumented) - getCredential(clusterDetails: ClusterDetails): Promise; + getCredential( + clusterDetails: ClusterDetails_2, + ): Promise; + // (undocumented) + presentAuthMetadata(_authMetadata: AuthMetadata_2): AuthMetadata_2; // (undocumented) validateCluster(): Error[]; } diff --git a/plugins/kubernetes-backend/src/auth/AksStrategy.ts b/plugins/kubernetes-backend/src/auth/AksStrategy.ts index 07bdf2efe0..40151314a8 100644 --- a/plugins/kubernetes-backend/src/auth/AksStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/AksStrategy.ts @@ -13,8 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ClusterDetails } from '../types/types'; -import { AuthenticationStrategy, KubernetesCredential } from './types'; +import { + AuthMetadata, + AuthenticationStrategy, + ClusterDetails, + KubernetesCredential, +} from '@backstage/plugin-kubernetes-node'; import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common'; /** @@ -31,7 +35,12 @@ export class AksStrategy implements AuthenticationStrategy { ? { type: 'bearer token', token: token as string } : { type: 'anonymous' }; } + public validateCluster(): Error[] { return []; } + + public presentAuthMetadata(_authMetadata: AuthMetadata): AuthMetadata { + return {}; + } } diff --git a/plugins/kubernetes-backend/src/auth/AnonymousStrategy.ts b/plugins/kubernetes-backend/src/auth/AnonymousStrategy.ts index 30a9593773..0f1aad398a 100644 --- a/plugins/kubernetes-backend/src/auth/AnonymousStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/AnonymousStrategy.ts @@ -14,7 +14,11 @@ * limitations under the License. */ -import { AuthenticationStrategy, KubernetesCredential } from './types'; +import { + AuthMetadata, + AuthenticationStrategy, + KubernetesCredential, +} from '@backstage/plugin-kubernetes-node'; /** * @@ -28,4 +32,8 @@ export class AnonymousStrategy implements AuthenticationStrategy { public validateCluster(): Error[] { return []; } + + public presentAuthMetadata(_authMetadata: AuthMetadata): AuthMetadata { + return {}; + } } diff --git a/plugins/kubernetes-backend/src/auth/AwsIamStrategy.ts b/plugins/kubernetes-backend/src/auth/AwsIamStrategy.ts index 53b880d60c..942b93615f 100644 --- a/plugins/kubernetes-backend/src/auth/AwsIamStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/AwsIamStrategy.ts @@ -25,8 +25,12 @@ import { ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE, ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID, } from '@backstage/plugin-kubernetes-common'; -import { ClusterDetails } from '../types/types'; -import { AuthenticationStrategy, KubernetesCredential } from './types'; +import { + AuthMetadata, + AuthenticationStrategy, + ClusterDetails, + KubernetesCredential, +} from '@backstage/plugin-kubernetes-node'; /** * @@ -128,4 +132,8 @@ export class AwsIamStrategy implements AuthenticationStrategy { return `k8s-aws-v1.${Buffer.from(url).toString('base64url')}`; } + + public presentAuthMetadata(_authMetadata: AuthMetadata): AuthMetadata { + return {}; + } } diff --git a/plugins/kubernetes-backend/src/auth/AzureIdentityStrategy.ts b/plugins/kubernetes-backend/src/auth/AzureIdentityStrategy.ts index d0eea3c13c..a1b29a10a1 100644 --- a/plugins/kubernetes-backend/src/auth/AzureIdentityStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/AzureIdentityStrategy.ts @@ -15,12 +15,16 @@ */ import { Logger } from 'winston'; -import { AuthenticationStrategy, KubernetesCredential } from './types'; import { AccessToken, DefaultAzureCredential, TokenCredential, } from '@azure/identity'; +import { + AuthMetadata, + AuthenticationStrategy, + KubernetesCredential, +} from '@backstage/plugin-kubernetes-node'; const aksScope = '6dae42f8-4368-4678-94ff-3960e28e3630/.default'; // This scope is the same for all Azure Managed Kubernetes @@ -89,4 +93,8 @@ export class AzureIdentityStrategy implements AuthenticationStrategy { private tokenExpired(): boolean { return Date.now() >= this.accessToken.expiresOnTimestamp; } + + public presentAuthMetadata(_authMetadata: AuthMetadata): AuthMetadata { + return {}; + } } diff --git a/plugins/kubernetes-backend/src/auth/DispatchStrategy.test.ts b/plugins/kubernetes-backend/src/auth/DispatchStrategy.test.ts index 7202311d6f..773e464168 100644 --- a/plugins/kubernetes-backend/src/auth/DispatchStrategy.test.ts +++ b/plugins/kubernetes-backend/src/auth/DispatchStrategy.test.ts @@ -31,6 +31,7 @@ describe('getCredential', () => { mockStrategy = { getCredential: jest.fn(), validateCluster: jest.fn(), + presentAuthMetadata: jest.fn(), }; strategy = new DispatchStrategy({ authStrategyMap: { google: mockStrategy }, diff --git a/plugins/kubernetes-backend/src/auth/DispatchStrategy.ts b/plugins/kubernetes-backend/src/auth/DispatchStrategy.ts index f12e49b577..14a3009a65 100644 --- a/plugins/kubernetes-backend/src/auth/DispatchStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/DispatchStrategy.ts @@ -14,12 +14,16 @@ * limitations under the License. */ -import { AuthenticationStrategy, KubernetesCredential } from './types'; -import { AuthMetadata, ClusterDetails } from '../types'; import { ANNOTATION_KUBERNETES_AUTH_PROVIDER, KubernetesRequestAuth, } from '@backstage/plugin-kubernetes-common'; +import { + AuthMetadata, + AuthenticationStrategy, + ClusterDetails, + KubernetesCredential, +} from '@backstage/plugin-kubernetes-node'; /** * @@ -67,4 +71,8 @@ export class DispatchStrategy implements AuthenticationStrategy { } return strategy.validateCluster(authMetadata); } + + public presentAuthMetadata(_authMetadata: AuthMetadata): AuthMetadata { + return {}; + } } diff --git a/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.ts b/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.ts index 2a44a31d6c..f6757d6a64 100644 --- a/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.ts @@ -13,7 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { AuthenticationStrategy, KubernetesCredential } from './types'; + +import { + AuthMetadata, + AuthenticationStrategy, + KubernetesCredential, +} from '@backstage/plugin-kubernetes-node'; import * as container from '@google-cloud/container'; /** @@ -36,4 +41,8 @@ export class GoogleServiceAccountStrategy implements AuthenticationStrategy { public validateCluster(): Error[] { return []; } + + public presentAuthMetadata(_authMetadata: AuthMetadata): AuthMetadata { + return {}; + } } diff --git a/plugins/kubernetes-backend/src/auth/GoogleStrategy.ts b/plugins/kubernetes-backend/src/auth/GoogleStrategy.ts index c1290dc1fe..c48086f7f8 100644 --- a/plugins/kubernetes-backend/src/auth/GoogleStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/GoogleStrategy.ts @@ -14,9 +14,13 @@ * limitations under the License. */ -import { AuthenticationStrategy, KubernetesCredential } from './types'; -import { ClusterDetails } from '../types/types'; import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common'; +import { + AuthMetadata, + AuthenticationStrategy, + ClusterDetails, + KubernetesCredential, +} from '@backstage/plugin-kubernetes-node'; /** * @@ -35,7 +39,12 @@ export class GoogleStrategy implements AuthenticationStrategy { } return { type: 'bearer token', token: token as string }; } + public validateCluster(): Error[] { return []; } + + public presentAuthMetadata(_authMetadata: AuthMetadata): AuthMetadata { + return {}; + } } diff --git a/plugins/kubernetes-backend/src/auth/OidcStrategy.ts b/plugins/kubernetes-backend/src/auth/OidcStrategy.ts index 445799cbb8..1039f1d40c 100644 --- a/plugins/kubernetes-backend/src/auth/OidcStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/OidcStrategy.ts @@ -18,8 +18,12 @@ import { ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER, KubernetesRequestAuth, } from '@backstage/plugin-kubernetes-common'; -import { AuthenticationStrategy, KubernetesCredential } from './types'; -import { AuthMetadata, ClusterDetails } from '../types/types'; +import { + AuthMetadata, + AuthenticationStrategy, + ClusterDetails, + KubernetesCredential, +} from '@backstage/plugin-kubernetes-node'; /** * @@ -57,4 +61,8 @@ export class OidcStrategy implements AuthenticationStrategy { } return []; } + + public presentAuthMetadata(_authMetadata: AuthMetadata): AuthMetadata { + return {}; + } } diff --git a/plugins/kubernetes-backend/src/auth/ServiceAccountStrategy.ts b/plugins/kubernetes-backend/src/auth/ServiceAccountStrategy.ts index 714b2a94d1..aff6e61a64 100644 --- a/plugins/kubernetes-backend/src/auth/ServiceAccountStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/ServiceAccountStrategy.ts @@ -14,10 +14,14 @@ * limitations under the License. */ +import { + AuthMetadata, + AuthenticationStrategy, + ClusterDetails, + KubernetesCredential, +} from '@backstage/plugin-kubernetes-node'; import { KubeConfig, User } from '@kubernetes/client-node'; import fs from 'fs-extra'; -import { AuthenticationStrategy, KubernetesCredential } from './types'; -import { ClusterDetails } from '../types/types'; /** * @@ -45,4 +49,8 @@ export class ServiceAccountStrategy implements AuthenticationStrategy { public validateCluster(): Error[] { return []; } + + public presentAuthMetadata(_authMetadata: AuthMetadata): AuthMetadata { + return {}; + } } diff --git a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts index 1720ef5ed6..ad3203a16d 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts @@ -32,6 +32,7 @@ describe('ConfigClusterLocator', () => { authStrategy = { getCredential: jest.fn(), validateCluster: jest.fn().mockReturnValue([]), + presentAuthMetadata: jest.fn(), }; }); diff --git a/plugins/kubernetes-backend/src/cluster-locator/index.test.ts b/plugins/kubernetes-backend/src/cluster-locator/index.test.ts index ded0faac5b..beb9fc9cfe 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/index.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/index.test.ts @@ -53,6 +53,7 @@ describe('getCombinedClusterSupplier', () => { const mockStrategy: jest.Mocked = { getCredential: jest.fn(), validateCluster: jest.fn().mockReturnValue([]), + presentAuthMetadata: jest.fn(), }; const clusterSupplier = getCombinedClusterSupplier( diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts index 42f953a2fe..5d19c6d94d 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts @@ -16,6 +16,9 @@ import { ANNOTATION_KUBERNETES_AUTH_PROVIDER, + ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE, + ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID, + ANNOTATION_KUBERNETES_DASHBOARD_URL, ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER, KubernetesRequestAuth, } from '@backstage/plugin-kubernetes-common'; @@ -44,6 +47,7 @@ import { createBackendModule, } from '@backstage/backend-plugin-api'; import { + AuthMetadata, KubernetesObjectsProvider, kubernetesAuthStrategyExtensionPoint, kubernetesClusterSupplierExtensionPoint, @@ -177,6 +181,103 @@ describe('API integration tests', () => { ], }); }); + + it('happy path: lists clusters with custom AuthStrategy and custom auth metadata', async () => { + const { server } = await startTestBackend({ + features: [ + minimalValidConfigService, + import('@backstage/plugin-kubernetes-backend/alpha'), + createBackendModule({ + pluginId: 'kubernetes', + moduleId: 'testObjectsProvider', + register(env) { + env.registerInit({ + deps: { extension: kubernetesObjectsProviderExtensionPoint }, + async init({ extension }) { + extension.addObjectsProvider(objectsProviderMock); + }, + }); + }, + }), + withClusters([ + { + name: 'some-cluster', + url: 'https://localhost:1234', + authMetadata: { + [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'customAuth', + [ANNOTATION_KUBERNETES_DASHBOARD_URL]: + 'https://127.0.0.1:8443/dashboard', + [ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID]: '12650152165654', + [ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE]: 'my_aws_role', + }, + }, + ]), + createBackendModule({ + pluginId: 'kubernetes', + moduleId: 'testAuthStrategy', + register(env) { + env.registerInit({ + deps: { extension: kubernetesAuthStrategyExtensionPoint }, + async init({ extension }) { + extension.addAuthStrategy('customAuth', { + getCredential: jest + .fn< + Promise, + [ClusterDetails, KubernetesRequestAuth] + >() + .mockImplementation(async (_, requestAuth) => ({ + type: 'bearer token', + token: requestAuth.custom as string, + })), + validateCluster: jest.fn().mockReturnValue([]), + presentAuthMetadata: ( + authMetadata: AuthMetadata, + ): AuthMetadata => { + const authMetadataFilter = Object.entries(authMetadata) + .filter(([key, _value]) => { + return [ + ANNOTATION_KUBERNETES_DASHBOARD_URL, + ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE, + ].includes(key); + }) + .reduce( + ( + accumulator: AuthMetadata, + currentValue: [string, string], + ) => { + accumulator[currentValue[0]] = currentValue[1]; + return accumulator; + }, + {}, + ); + + return authMetadataFilter; + }, + }); + }, + }); + }, + }), + ], + }); + app = server; + + const response = await request(app).get('/api/kubernetes/clusters'); + + expect(response.status).toEqual(200); + expect(response.body).toStrictEqual({ + items: [ + { + name: 'some-cluster', + authProvider: 'customAuth', + auth: { + 'kubernetes.io/aws-assume-role': 'my_aws_role', + 'kubernetes.io/dashboard-url': 'https://127.0.0.1:8443/dashboard', + }, + }, + ], + }); + }); }); describe('post /services/:serviceId', () => { @@ -340,6 +441,7 @@ describe('API integration tests', () => { token: requestAuth.custom as string, })), validateCluster: jest.fn().mockReturnValue([]), + presentAuthMetadata: jest.fn().mockReturnValue({}), }); }, }); @@ -508,6 +610,7 @@ metadata: >() .mockResolvedValue({ type: 'anonymous' }), validateCluster: jest.fn().mockReturnValue([]), + presentAuthMetadata: jest.fn().mockReturnValue({}), }); }, }); @@ -530,6 +633,7 @@ metadata: const authStrategy = { getCredential: jest.fn().mockResolvedValue({ type: 'anonymous' }), validateCluster: jest.fn().mockReturnValue([]), + presentAuthMetadata: jest.fn().mockReturnValue({}), }; worker.use( rest.get('http://my.cluster/api', (_req, res, ctx) => @@ -613,6 +717,7 @@ metadata: >() .mockResolvedValue({ type: 'anonymous' }), validateCluster: jest.fn().mockReturnValue([]), + presentAuthMetadata: jest.fn().mockReturnValue({}), }); }, }); diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts index a4a577db85..81cc3d471d 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts @@ -29,7 +29,6 @@ import { Logger } from 'winston'; import { getCombinedClusterSupplier } from '../cluster-locator'; import { - AuthenticationStrategy, AnonymousStrategy, DispatchStrategy, GoogleStrategy, @@ -45,17 +44,19 @@ import { addResourceRoutesToRouter } from '../routes/resourcesRoutes'; import { MultiTenantServiceLocator } from '../service-locator/MultiTenantServiceLocator'; import { SingleTenantServiceLocator } from '../service-locator/SingleTenantServiceLocator'; import { - CustomResource, - KubernetesFetcher, KubernetesObjectsProviderOptions, - KubernetesObjectTypes, - KubernetesServiceLocator, ObjectsByEntityRequest, ServiceLocatorMethod, } from '../types/types'; import { + AuthenticationStrategy, + AuthMetadata, + CustomResource, KubernetesClustersSupplier, + KubernetesFetcher, KubernetesObjectsProvider, + KubernetesObjectTypes, + KubernetesServiceLocator, } from '@backstage/plugin-kubernetes-node'; import { DEFAULT_OBJECTS, @@ -369,11 +370,20 @@ export class KubernetesBuilder { items: clusterDetails.map(cd => { const oidcTokenProvider = cd.authMetadata[ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER]; + const authProvider = + cd.authMetadata[ANNOTATION_KUBERNETES_AUTH_PROVIDER]; + const strategy = this.getAuthStrategyMap()[authProvider]; + let auth: AuthMetadata = {}; + if (strategy) { + auth = strategy.presentAuthMetadata(cd.authMetadata); + } + return { name: cd.name, dashboardUrl: cd.dashboardUrl, - authProvider: cd.authMetadata[ANNOTATION_KUBERNETES_AUTH_PROVIDER], + authProvider, ...(oidcTokenProvider && { oidcTokenProvider }), + ...(auth && Object.keys(auth).length !== 0 && { auth }), }; }), }); diff --git a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts index 1faf2f0673..a50d0b37a2 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts @@ -196,6 +196,7 @@ describe('KubernetesFanOutHandler', () => { >() .mockResolvedValue({ type: 'anonymous' }), validateCluster: jest.fn().mockReturnValue([]), + presentAuthMetadata: jest.fn(), }, config, }); @@ -1147,6 +1148,7 @@ describe('KubernetesFanOutHandler', () => { >() .mockResolvedValue({ type: 'bearer token', token: 'token' }), validateCluster: jest.fn().mockReturnValue([]), + presentAuthMetadata: jest.fn(), }, config, }); diff --git a/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts b/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts index 94a8b2918e..f2eb61e7f9 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts @@ -131,6 +131,7 @@ describe('KubernetesProxy', () => { >() .mockResolvedValue({ type: 'anonymous' }), validateCluster: jest.fn(), + presentAuthMetadata: jest.fn(), }; proxy = new KubernetesProxy({ logger, clusterSupplier, authStrategy }); permissionApi.authorize.mockResolvedValue([ @@ -514,6 +515,7 @@ describe('KubernetesProxy', () => { .fn() .mockReturnValue({ type: 'bearer token', token: 'MY_TOKEN3' }), validateCluster: jest.fn(), + presentAuthMetadata: jest.fn(), }; proxy = new KubernetesProxy({ diff --git a/plugins/kubernetes-node/api-report.md b/plugins/kubernetes-node/api-report.md index 4c858ec969..7afac72574 100644 --- a/plugins/kubernetes-node/api-report.md +++ b/plugins/kubernetes-node/api-report.md @@ -25,6 +25,8 @@ export interface AuthenticationStrategy { authConfig: KubernetesRequestAuth, ): Promise; // (undocumented) + presentAuthMetadata(authMetadata: AuthMetadata): AuthMetadata; + // (undocumented) validateCluster(authMetadata: AuthMetadata): Error[]; } diff --git a/plugins/kubernetes-node/src/types/types.ts b/plugins/kubernetes-node/src/types/types.ts index 036b54ed98..d7b2b67265 100644 --- a/plugins/kubernetes-node/src/types/types.ts +++ b/plugins/kubernetes-node/src/types/types.ts @@ -152,6 +152,7 @@ export interface AuthenticationStrategy { authConfig: KubernetesRequestAuth, ): Promise; validateCluster(authMetadata: AuthMetadata): Error[]; + presentAuthMetadata(authMetadata: AuthMetadata): AuthMetadata; } /**