diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/OidcKubernetesAuthTranslator.test.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/OidcKubernetesAuthTranslator.test.ts index 3c8edd38b4..1aacb7f059 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/OidcKubernetesAuthTranslator.test.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/OidcKubernetesAuthTranslator.test.ts @@ -38,7 +38,7 @@ describe('OidcKubernetesAuthTranslator tests', () => { ...baseClusterDetails, }, { - auth: { okta: 'fakeToken' }, + oidc: { okta: 'fakeToken' }, entity, }, ); @@ -60,6 +60,6 @@ describe('OidcKubernetesAuthTranslator tests', () => { { oidcTokenProvider: 'okta', ...baseClusterDetails }, { entity }, ), - ).rejects.toThrow('Auth token not found under auth.okta in request body'); + ).rejects.toThrow('Auth token not found under oidc.okta in request body'); }); }); diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/OidcKubernetesAuthTranslator.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/OidcKubernetesAuthTranslator.ts index cbcbd3c6ab..05b87f0ce0 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/OidcKubernetesAuthTranslator.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/OidcKubernetesAuthTranslator.ts @@ -36,13 +36,13 @@ export class OidcKubernetesAuthTranslator implements KubernetesAuthTranslator { ); } - const authToken: string | undefined = requestBody.auth?.[oidcTokenProvider]; + const authToken: string | undefined = requestBody.oidc?.[oidcTokenProvider]; if (authToken) { clusterDetailsWithAuthToken.serviceAccountToken = authToken; } else { throw new Error( - `Auth token not found under auth.${oidcTokenProvider} in request body`, + `Auth token not found under oidc.${oidcTokenProvider} in request body`, ); } return clusterDetailsWithAuthToken; diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts index a3dfc8b005..18c3fdc931 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts @@ -246,6 +246,7 @@ export class KubernetesBuilder { name: cd.name, dashboardUrl: cd.dashboardUrl, authProvider: cd.authProvider, + oidcTokenProvider: cd.oidcTokenProvider, })), }); }); diff --git a/plugins/kubernetes-common/src/types.ts b/plugins/kubernetes-common/src/types.ts index 135be4099c..a71d1feaad 100644 --- a/plugins/kubernetes-common/src/types.ts +++ b/plugins/kubernetes-common/src/types.ts @@ -30,6 +30,9 @@ import { Entity } from '@backstage/catalog-model'; export interface KubernetesRequestBody { auth?: { + google: string; + }; + oidc?: { [key: string]: string; }; entity: Entity; diff --git a/plugins/kubernetes/src/api/types.ts b/plugins/kubernetes/src/api/types.ts index 8b95dd820c..7c5943a6eb 100644 --- a/plugins/kubernetes/src/api/types.ts +++ b/plugins/kubernetes/src/api/types.ts @@ -28,5 +28,11 @@ export interface KubernetesApi { getObjectsByEntity( requestBody: KubernetesRequestBody, ): Promise; - getClusters(): Promise<{ name: string; authProvider: string }[]>; + getClusters(): Promise< + { + name: string; + authProvider: string; + oidcTokenProvider?: string | undefined; + }[] + >; } diff --git a/plugins/kubernetes/src/hooks/useKubernetesObjects.ts b/plugins/kubernetes/src/hooks/useKubernetesObjects.ts index d9aca875c7..8e2597c1d2 100644 --- a/plugins/kubernetes/src/hooks/useKubernetesObjects.ts +++ b/plugins/kubernetes/src/hooks/useKubernetesObjects.ts @@ -53,8 +53,16 @@ export const useKubernetesObjects = ( } const authProviders: string[] = [ - ...new Set(clusters.map(c => c.authProvider)), + ...new Set( + clusters.map( + c => + `${c.authProvider}${ + c.oidcTokenProvider ? `.${c.oidcTokenProvider}` : '' + }`, + ), + ), ]; + // For each auth type, invoke decorateRequestBodyForAuth on corresponding KubernetesAuthProvider let requestBody: KubernetesRequestBody = { entity, diff --git a/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.test.ts b/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.test.ts new file mode 100644 index 0000000000..ffeaa6137c --- /dev/null +++ b/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.test.ts @@ -0,0 +1,79 @@ +/* + * Copyright 2020 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 { OAuthApi, OpenIdConnectApi } from '@backstage/core-plugin-api'; +import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common'; +import { KubernetesAuthProviders } from './KubernetesAuthProviders'; + +class MockAuthApi implements OAuthApi, OpenIdConnectApi { + constructor(private readonly token: string) {} + + getAccessToken = jest.fn(async () => { + return this.token; + }); + + getIdToken = jest.fn(async () => { + return this.token; + }); +} + +const requestBody: KubernetesRequestBody = { + entity: { + apiVersion: 'v1', + kind: 'service', + metadata: { name: 'test' }, + }, +}; + +describe('KubernetesAuthProviders tests', () => { + const kap = new KubernetesAuthProviders({ + googleAuthApi: new MockAuthApi('googleToken'), + oidcProviders: { + okta: new MockAuthApi('oktaToken'), + }, + }); + + it('adds token to request body for google authProvider', async () => { + const details = await kap.decorateRequestBodyForAuth('google', requestBody); + + expect(details.auth?.google).toBe('googleToken'); + }); + + it('adds token to request body for oidc authProvider', async () => { + const details = await kap.decorateRequestBodyForAuth( + 'oidc.okta', + requestBody, + ); + + expect(details.oidc?.okta).toBe('oktaToken'); + }); + + it('returns error for unknown authProvider', async () => { + await expect( + kap.decorateRequestBodyForAuth('unknown', requestBody), + ).rejects.toThrow( + 'authProvider "unknown" has no KubernetesAuthProvider defined for it', + ); + }); + + it('returns error for missconfigured oidc authProvider', async () => { + await expect( + kap.decorateRequestBodyForAuth('oidc.random', requestBody), + ).rejects.toThrow( + 'KubernetesAuthProviders has no oidcProvider configured for oidc.random', + ); + }); +}); diff --git a/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.ts b/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.ts index 00d5a17f72..bcd1b2911a 100644 --- a/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.ts +++ b/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.ts @@ -19,9 +19,10 @@ import { KubernetesAuthProvider, KubernetesAuthProvidersApi } from './types'; import { GoogleKubernetesAuthProvider } from './GoogleKubernetesAuthProvider'; import { ServiceAccountKubernetesAuthProvider } from './ServiceAccountKubernetesAuthProvider'; import { AwsKubernetesAuthProvider } from './AwsKubernetesAuthProvider'; -import { OAuthApi } from '@backstage/core-plugin-api'; +import { OAuthApi, OpenIdConnectApi } from '@backstage/core-plugin-api'; import { GoogleServiceAccountAuthProvider } from './GoogleServiceAccountAuthProvider'; import { AzureKubernetesAuthProvider } from './AzureKubernetesAuthProvider'; +import { OidcKubernetesAuthProvider } from './OidcKubernetesAuthProvider'; export class KubernetesAuthProviders implements KubernetesAuthProvidersApi { private readonly kubernetesAuthProviderMap: Map< @@ -29,7 +30,10 @@ export class KubernetesAuthProviders implements KubernetesAuthProvidersApi { KubernetesAuthProvider >; - constructor(options: { googleAuthApi: OAuthApi }) { + constructor(options: { + googleAuthApi: OAuthApi; + oidcProviders?: { [key: string]: OpenIdConnectApi }; + }) { this.kubernetesAuthProviderMap = new Map(); this.kubernetesAuthProviderMap.set( 'google', @@ -48,6 +52,18 @@ export class KubernetesAuthProviders implements KubernetesAuthProvidersApi { 'azure', new AzureKubernetesAuthProvider(), ); + + if (options.oidcProviders) { + Object.keys(options.oidcProviders).forEach(provider => { + this.kubernetesAuthProviderMap.set( + `oidc.${provider}`, + new OidcKubernetesAuthProvider( + provider, + options.oidcProviders![provider], + ), + ); + }); + } } async decorateRequestBodyForAuth( @@ -61,6 +77,12 @@ export class KubernetesAuthProviders implements KubernetesAuthProvidersApi { requestBody, ); } + + if (authProvider.startsWith('oidc.')) { + throw new Error( + `KubernetesAuthProviders has no oidcProvider configured for ${authProvider}`, + ); + } throw new Error( `authProvider "${authProvider}" has no KubernetesAuthProvider defined for it`, ); diff --git a/plugins/kubernetes/src/kubernetes-auth-provider/OidcKubernetesAuthProvider.ts b/plugins/kubernetes/src/kubernetes-auth-provider/OidcKubernetesAuthProvider.ts new file mode 100644 index 0000000000..f308c09c2d --- /dev/null +++ b/plugins/kubernetes/src/kubernetes-auth-provider/OidcKubernetesAuthProvider.ts @@ -0,0 +1,41 @@ +/* + * Copyright 2020 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 { KubernetesAuthProvider } from './types'; +import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common'; +import { OpenIdConnectApi } from '@backstage/core-plugin-api'; + +export class OidcKubernetesAuthProvider implements KubernetesAuthProvider { + providerName: string; + authProvider: OpenIdConnectApi; + + constructor(providerName: string, authProvider: OpenIdConnectApi) { + this.providerName = providerName; + this.authProvider = authProvider; + } + + async decorateRequestBodyForAuth( + requestBody: KubernetesRequestBody, + ): Promise { + const authToken: string = await this.authProvider.getIdToken(); + if ('oidc' in requestBody) { + requestBody.oidc![this.providerName] = authToken; + } else { + requestBody.oidc = { [this.providerName]: authToken }; + } + return requestBody; + } +} diff --git a/plugins/kubernetes/src/plugin.ts b/plugins/kubernetes/src/plugin.ts index a1f2f57621..b54850307e 100644 --- a/plugins/kubernetes/src/plugin.ts +++ b/plugins/kubernetes/src/plugin.ts @@ -24,6 +24,9 @@ import { discoveryApiRef, identityApiRef, googleAuthApiRef, + microsoftAuthApiRef, + oktaAuthApiRef, + oneloginAuthApiRef, createRoutableExtension, } from '@backstage/core-plugin-api'; @@ -45,9 +48,26 @@ export const kubernetesPlugin = createPlugin({ }), createApiFactory({ api: kubernetesAuthProvidersApiRef, - deps: { googleAuthApi: googleAuthApiRef }, - factory: ({ googleAuthApi }) => { - return new KubernetesAuthProviders({ googleAuthApi }); + deps: { + googleAuthApi: googleAuthApiRef, + microsoftAuthApi: microsoftAuthApiRef, + oktaAuthApi: oktaAuthApiRef, + oneloginAuthApi: oneloginAuthApiRef, + }, + factory: ({ + googleAuthApi, + microsoftAuthApi, + oktaAuthApi, + oneloginAuthApi, + }) => { + const oidcProviders = { + google: googleAuthApi, + microsoft: microsoftAuthApi, + okta: oktaAuthApi, + onelogin: oneloginAuthApi, + }; + + return new KubernetesAuthProviders({ googleAuthApi, oidcProviders }); }, }), ],