Allow for Kubernetes cluster authentication via google auth (#2691)

* Allow for Kubernetes cluster authentication via google auth

- For the kubernetes and kubernetes-backend plugins
  - The kubernetes (front-end) plugin uses the googleAuthApi and goes
    through oauth flow to fetch a Google auth token for the current user
    and pass that in a request to the kubernetes-backend plugin, which
    uses this token as the service account token (and hence as the
    authentication token for making K8s API requests).
- Related to https://github.com/spotify/backstage/issues/2552

* Use KubernetesAuthProvider and KubernetesAuthTranslator interfaces for K8s auth

- Implementations of KubernetesAuthProvider decorate the bodies of requests (for
  Kubernetes resources) sent from the kubernetes plugin (frontend) to the
  kubernetes-backend plugin (backend) with whatever information is
  needed for K8s authentication
- Implementations of KubernetesAuthTranslator take the contents of these request
  bodies sent from the kubernetes plugin (frontend) to the kubernetes-backend plugin
  (backend) and use specific values in the bodies to properly set up tokens for K8s auth
- Start with KubernetesAuthProvider + KubernetesAuthTranslator
  implementations for 'serviceAccount' and 'google' as auth providers
- Implementation of what was proposed at https://github.com/spotify/backstage/issues/2552#issuecomment-702545382
- Load in and prepare KubernetesAuthProvider implementations at plugin
  startup time via KubernetesAuthProviders API (that essentially stores
  or wraps these KubernetesAuthProvider instances)
- Related to https://github.com/spotify/backstage/issues/2552
This commit is contained in:
James Wen
2020-10-07 10:21:40 -04:00
committed by GitHub
parent c1de0de4bd
commit 108ce37949
24 changed files with 524 additions and 34 deletions
@@ -43,6 +43,7 @@ describe('MultiTenantConfigClusterLocator', () => {
{
name: 'cluster1',
url: 'http://localhost:8080',
authProvider: 'serviceAccount',
},
],
},
@@ -60,6 +61,7 @@ describe('MultiTenantConfigClusterLocator', () => {
name: 'cluster1',
serviceAccountToken: undefined,
url: 'http://localhost:8080',
authProvider: 'serviceAccount',
},
]);
});
@@ -70,13 +72,14 @@ describe('MultiTenantConfigClusterLocator', () => {
clusters: [
{
name: 'cluster1',
serviceAccountToken: undefined,
serviceAccountToken: 'token',
url: 'http://localhost:8080',
authProvider: 'serviceAccount',
},
{
name: 'cluster2',
serviceAccountToken: undefined,
url: 'http://localhost:8081',
authProvider: 'google',
},
],
},
@@ -92,13 +95,15 @@ describe('MultiTenantConfigClusterLocator', () => {
expect(result).toStrictEqual([
{
name: 'cluster1',
serviceAccountToken: undefined,
serviceAccountToken: 'token',
url: 'http://localhost:8080',
authProvider: 'serviceAccount',
},
{
name: 'cluster2',
serviceAccountToken: undefined,
url: 'http://localhost:8081',
authProvider: 'google',
},
]);
});
@@ -28,12 +28,15 @@ export class MultiTenantConfigClusterLocator
}
static fromConfig(config: Config[]): MultiTenantConfigClusterLocator {
// TODO: Add validation that authProvider is required and serviceAccountToken
// is required if authProvider is serviceAccount
return new MultiTenantConfigClusterLocator(
config.map(c => {
return {
name: c.getString('name'),
url: c.getString('url'),
serviceAccountToken: c.getOptionalString('serviceAccountToken'),
authProvider: c.getString('authProvider'),
};
}),
);
@@ -15,3 +15,4 @@
*/
export type ClusterLocatorMethod = 'configMultiTenant' | 'http';
export type AuthProviderType = 'google' | 'serviceAccount';
@@ -0,0 +1,41 @@
/*
* Copyright 2020 Spotify AB
*
* 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 { KubernetesAuthTranslator } from './types';
import { AuthRequestBody, ClusterDetails } from '../types/types';
export class GoogleKubernetesAuthTranslator
implements KubernetesAuthTranslator {
async decorateClusterDetailsWithAuth(
clusterDetails: ClusterDetails,
requestBody: AuthRequestBody,
): Promise<ClusterDetails> {
const clusterDetailsWithAuthToken: ClusterDetails = Object.assign(
{},
clusterDetails,
);
const authToken: string | undefined = requestBody.auth?.google;
if (authToken) {
clusterDetailsWithAuthToken.serviceAccountToken = authToken;
} else {
throw new Error(
'Google token not found under auth.google in request body',
);
}
return clusterDetailsWithAuthToken;
}
}
@@ -0,0 +1,46 @@
/*
* Copyright 2020 Spotify AB
*
* 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 { KubernetesAuthTranslator } from './types';
import { GoogleKubernetesAuthTranslator } from './GoogleKubernetesAuthTranslator';
import { KubernetesAuthTranslatorGenerator } from './KubernetesAuthTranslatorGenerator';
import { ServiceAccountKubernetesAuthTranslator } from './ServiceAccountKubernetesAuthTranslator';
describe('getKubernetesAuthTranslatorInstance', () => {
const sut = KubernetesAuthTranslatorGenerator;
it('can return an auth translator for google auth', () => {
const authTranslator: KubernetesAuthTranslator = sut.getKubernetesAuthTranslatorInstance(
'google',
);
expect(authTranslator instanceof GoogleKubernetesAuthTranslator).toBe(true);
});
it('can return an auth translator for serviceAccount auth', () => {
const authTranslator: KubernetesAuthTranslator = sut.getKubernetesAuthTranslatorInstance(
'serviceAccount',
);
expect(
authTranslator instanceof ServiceAccountKubernetesAuthTranslator,
).toBe(true);
});
it('throws an error when asked for an auth translator for an unsupported auth type', () => {
expect(() => sut.getKubernetesAuthTranslatorInstance('linode')).toThrow(
'authProvider "linode" has no KubernetesAuthTranslator associated with it',
);
});
});
@@ -0,0 +1,39 @@
/*
* Copyright 2020 Spotify AB
*
* 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 { KubernetesAuthTranslator } from './types';
import { GoogleKubernetesAuthTranslator } from './GoogleKubernetesAuthTranslator';
import { ServiceAccountKubernetesAuthTranslator } from './ServiceAccountKubernetesAuthTranslator';
export class KubernetesAuthTranslatorGenerator {
static getKubernetesAuthTranslatorInstance(
authProvider: String,
): KubernetesAuthTranslator {
switch (authProvider) {
case 'google': {
return new GoogleKubernetesAuthTranslator();
}
case 'serviceAccount': {
return new ServiceAccountKubernetesAuthTranslator();
}
default: {
throw new Error(
`authProvider "${authProvider}" has no KubernetesAuthTranslator associated with it`,
);
}
}
}
}
@@ -0,0 +1,31 @@
/*
* Copyright 2020 Spotify AB
*
* 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 { KubernetesAuthTranslator } from './types';
import { AuthRequestBody, ClusterDetails } from '../types/types';
export class ServiceAccountKubernetesAuthTranslator
implements KubernetesAuthTranslator {
async decorateClusterDetailsWithAuth(
clusterDetails: ClusterDetails,
// To ignore TS6133 linting error where it detects 'requestBody' is declared but its value is never read.
// @ts-ignore-start
requestBody: AuthRequestBody, // eslint-disable-line @typescript-eslint/no-unused-vars
// @ts-ignore-end
): Promise<ClusterDetails> {
return clusterDetails;
}
}
@@ -0,0 +1,24 @@
/*
* Copyright 2020 Spotify AB
*
* 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 { AuthRequestBody, ClusterDetails } from '../types/types';
export interface KubernetesAuthTranslator {
decorateClusterDetailsWithAuth(
clusterDetails: ClusterDetails,
requestBody: AuthRequestBody,
): Promise<ClusterDetails>;
}
@@ -33,6 +33,7 @@ describe('KubernetesClientProvider', () => {
name: 'cluster-name',
url: 'http://localhost:9999',
serviceAccountToken: 'TOKEN',
authProvider: 'serviceAccount',
});
expect(result.basePath).toBe('http://localhost:9999');
@@ -55,12 +56,14 @@ describe('KubernetesClientProvider', () => {
name: 'cluster-name',
url: 'http://localhost:9999',
serviceAccountToken: 'TOKEN',
authProvider: 'serviceAccount',
});
const result2 = sut.getCoreClientByClusterDetails({
name: 'cluster-name',
url: 'http://localhost:9999',
serviceAccountToken: 'TOKEN',
authProvider: 'serviceAccount',
});
expect(result1.basePath).toBe('http://localhost:9999');
@@ -89,6 +92,7 @@ describe('KubernetesClientProvider', () => {
name: 'cluster-name',
url: 'http://localhost:9999',
serviceAccountToken: 'TOKEN',
authProvider: 'serviceAccount',
});
expect(result.basePath).toBe('http://localhost:9999');
@@ -111,12 +115,14 @@ describe('KubernetesClientProvider', () => {
name: 'cluster-name',
url: 'http://localhost:9999',
serviceAccountToken: 'TOKEN',
authProvider: 'serviceAccount',
});
const result2 = sut.getAppsClientByClusterDetails({
name: 'cluster-name',
url: 'http://localhost:9999',
serviceAccountToken: 'TOKEN',
authProvider: 'serviceAccount',
});
expect(result1.basePath).toBe('http://localhost:9999');
@@ -62,7 +62,8 @@ describe('KubernetesClientProvider', () => {
{
name: 'cluster1',
url: 'http://localhost:9999',
serviceAccountToken: undefined,
serviceAccountToken: 'token',
authProvider: 'serviceAccount',
},
new Set(['pods', 'services']),
);
@@ -124,7 +125,8 @@ describe('KubernetesClientProvider', () => {
{
name: 'cluster1',
url: 'http://localhost:9999',
serviceAccountToken: undefined,
serviceAccountToken: 'token',
authProvider: 'serviceAccount',
},
new Set(['pods', 'services']),
);
@@ -172,7 +174,8 @@ describe('KubernetesClientProvider', () => {
{
name: 'cluster1',
url: 'http://localhost:9999',
serviceAccountToken: undefined,
serviceAccountToken: 'token',
authProvider: 'serviceAccount',
},
new Set<any>(['foo']),
),
@@ -74,6 +74,7 @@ describe('handleGetKubernetesObjectsByServiceId', () => {
Promise.resolve([
{
name: 'test-cluster',
authProvider: 'serviceAccount',
},
]),
);
@@ -89,6 +90,7 @@ describe('handleGetKubernetesObjectsByServiceId', () => {
getClusterByServiceId,
},
getVoidLogger(),
{},
);
expect(getClusterByServiceId.mock.calls.length).toBe(1);
@@ -142,9 +144,11 @@ describe('handleGetKubernetesObjectsByServiceId', () => {
Promise.resolve([
{
name: 'test-cluster',
authProvider: 'serviceAccount',
},
{
name: 'other-cluster',
authProvider: 'google',
},
]),
);
@@ -160,6 +164,11 @@ describe('handleGetKubernetesObjectsByServiceId', () => {
getClusterByServiceId,
},
getVoidLogger(),
{
auth: {
google: 'google_token_123',
},
},
);
expect(getClusterByServiceId.mock.calls.length).toBe(1);
@@ -16,17 +16,22 @@
import { Logger } from 'winston';
import {
AuthRequestBody,
ClusterDetails,
KubernetesClusterLocator,
KubernetesFetcher,
KubernetesObjectTypes,
ObjectsByServiceIdResponse,
} from '..';
} from '../types/types';
import { KubernetesAuthTranslator } from '../kubernetes-auth-translator/types';
import { KubernetesAuthTranslatorGenerator } from '../kubernetes-auth-translator/KubernetesAuthTranslatorGenerator';
export type GetKubernetesObjectsByServiceIdHandler = (
serviceId: string,
fetcher: KubernetesFetcher,
clusterLocator: KubernetesClusterLocator,
logger: Logger,
requestBody: AuthRequestBody,
objectsToFetch?: Set<KubernetesObjectTypes>,
) => Promise<ObjectsByServiceIdResponse>;
@@ -46,18 +51,35 @@ export const handleGetKubernetesObjectsByServiceId: GetKubernetesObjectsByServic
fetcher,
clusterLocator,
logger,
requestBody,
objectsToFetch = DEFAULT_OBJECTS,
) => {
const clusterDetails = await clusterLocator.getClusterByServiceId(serviceId);
const clusterDetails: ClusterDetails[] = await clusterLocator.getClusterByServiceId(
serviceId,
);
// Execute all of these async actions simultaneously/without blocking sequentially as no common object is modified by them
const promises: Promise<ClusterDetails>[] = clusterDetails.map(cd => {
const kubernetesAuthTranslator: KubernetesAuthTranslator = KubernetesAuthTranslatorGenerator.getKubernetesAuthTranslatorInstance(
cd.authProvider,
);
return kubernetesAuthTranslator.decorateClusterDetailsWithAuth(
cd,
requestBody,
);
});
const clusterDetailsDecoratedForAuth: ClusterDetails[] = await Promise.all(
promises,
);
logger.info(
`serviceId=${serviceId} clusterDetails=[${clusterDetails
`serviceId=${serviceId} clusterDetails=[${clusterDetailsDecoratedForAuth
.map(c => c.name)
.join(', ')}]`,
);
return Promise.all(
clusterDetails.map(cd => {
clusterDetailsDecoratedForAuth.map(cd => {
return fetcher
.fetchObjectsByServiceId(serviceId, cd, objectsToFetch)
.then(result => {
@@ -54,8 +54,8 @@ describe('router', () => {
jest.resetAllMocks();
});
describe('GET /services/:serviceId', () => {
it('happy path: lists kubernetes objects', async () => {
describe('post /services/:serviceId', () => {
it('happy path: lists kubernetes objects without auth in request body', async () => {
const result = {
clusterOne: {
pods: [
@@ -69,7 +69,34 @@ describe('router', () => {
} as any;
handleGetByServiceId.mockReturnValueOnce(Promise.resolve(result));
const response = await request(app).get('/services/test-service');
const response = await request(app).post('/services/test-service');
expect(response.status).toEqual(200);
expect(response.body).toEqual(result);
});
it('happy path: lists kubernetes objects with auth in request body', async () => {
const result = {
clusterOne: {
pods: [
{
metadata: {
name: 'pod1',
},
},
],
},
} as any;
handleGetByServiceId.mockReturnValueOnce(Promise.resolve(result));
const response = await request(app)
.post('/services/test-service')
.send({
auth: {
google: 'google_token_123',
},
})
.set('Content-Type', 'application/json');
expect(response.status).toEqual(200);
expect(response.body).toEqual(result);
@@ -78,7 +105,7 @@ describe('router', () => {
it('internal error: lists kubernetes objects', async () => {
handleGetByServiceId.mockRejectedValue(Error('some internal error'));
const response = await request(app).get('/services/test-service');
const response = await request(app).post('/services/test-service');
expect(response.status).toEqual(500);
expect(response.body).toEqual({ error: 'some internal error' });
@@ -26,7 +26,11 @@ import {
GetKubernetesObjectsByServiceIdHandler,
handleGetKubernetesObjectsByServiceId,
} from './getKubernetesObjectsByServiceIdHandler';
import { KubernetesClusterLocator, KubernetesFetcher } from '..';
import {
AuthRequestBody,
KubernetesClusterLocator,
KubernetesFetcher,
} from '../types/types';
export interface RouterOptions {
logger: Logger;
@@ -62,15 +66,16 @@ export const makeRouter = (
router.use(express.json());
// TODO error handling
router.get('/services/:serviceId', async (req, res) => {
router.post('/services/:serviceId', async (req, res) => {
const serviceId = req.params.serviceId;
const requestBody: AuthRequestBody = req.body;
try {
const response = await handleGetByServiceId(
serviceId,
fetcher,
clusterLocator,
logger,
requestBody,
);
res.send(response);
} catch (e) {
@@ -27,8 +27,14 @@ import {
export interface ClusterDetails {
name: string;
url: string;
// TODO this will eventually be configured by the auth translation work
serviceAccountToken: string | undefined;
authProvider: string;
serviceAccountToken?: string | undefined;
}
export interface AuthRequestBody {
auth?: {
google?: string;
};
}
export interface ClusterObjects {
+1
View File
@@ -21,6 +21,7 @@
},
"dependencies": {
"@backstage/catalog-model": "^0.1.1-alpha.24",
"@backstage/config": "^0.1.1-alpha.24",
"@backstage/core": "^0.1.1-alpha.24",
"@backstage/plugin-kubernetes-backend": "^0.1.1-alpha.24",
"@backstage/theme": "^0.1.1-alpha.24",
@@ -16,7 +16,10 @@
import { DiscoveryApi } from '@backstage/core';
import { KubernetesApi } from './types';
import { ObjectsByServiceIdResponse } from '@backstage/plugin-kubernetes-backend';
import {
AuthRequestBody,
ObjectsByServiceIdResponse,
} from '@backstage/plugin-kubernetes-backend';
export class KubernetesBackendClient implements KubernetesApi {
private readonly discoveryApi: DiscoveryApi;
@@ -25,9 +28,18 @@ export class KubernetesBackendClient implements KubernetesApi {
this.discoveryApi = options.discoveryApi;
}
private async getRequired(path: string): Promise<any> {
private async getRequired(
path: string,
requestBody: AuthRequestBody,
): Promise<any> {
const url = `${await this.discoveryApi.getBaseUrl('kubernetes')}${path}`;
const response = await fetch(url);
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody),
});
if (!response.ok) {
const payload = await response.text();
@@ -40,7 +52,8 @@ export class KubernetesBackendClient implements KubernetesApi {
async getObjectsByServiceId(
serviceId: String,
requestBody: AuthRequestBody,
): Promise<ObjectsByServiceIdResponse> {
return await this.getRequired(`/services/${serviceId}`);
return await this.getRequired(`/services/${serviceId}`, requestBody);
}
}
+8 -2
View File
@@ -15,7 +15,10 @@
*/
import { createApiRef } from '@backstage/core';
import { ObjectsByServiceIdResponse } from '@backstage/plugin-kubernetes-backend';
import {
AuthRequestBody,
ObjectsByServiceIdResponse,
} from '@backstage/plugin-kubernetes-backend';
export const kubernetesApiRef = createApiRef<KubernetesApi>({
id: 'plugin.kubernetes.service',
@@ -24,5 +27,8 @@ export const kubernetesApiRef = createApiRef<KubernetesApi>({
});
export interface KubernetesApi {
getObjectsByServiceId(serviceId: String): Promise<ObjectsByServiceIdResponse>;
getObjectsByServiceId(
serviceId: String,
requestBody: AuthRequestBody,
): Promise<ObjectsByServiceIdResponse>;
}
@@ -16,8 +16,10 @@
import React, { ReactElement, useEffect, useState } from 'react';
import { Grid, TabProps } from '@material-ui/core';
import { Config } from '@backstage/config';
import {
CardTab,
configApiRef,
Content,
Page,
pageTheme,
@@ -28,10 +30,12 @@ import {
import { Entity } from '@backstage/catalog-model';
import { kubernetesApiRef } from '../../api/types';
import {
AuthRequestBody,
ClusterObjects,
FetchResponse,
ObjectsByServiceIdResponse,
} from '@backstage/plugin-kubernetes-backend';
import { kubernetesAuthProvidersApiRef } from '../../kubernetes-auth-provider/types';
import { DeploymentTables } from '../DeploymentTables';
import { DeploymentTriple } from '../../types/types';
import {
@@ -105,16 +109,40 @@ export const KubernetesContent = ({ entity }: KubernetesContentProps) => {
>(undefined);
const [error, setError] = useState<string | undefined>(undefined);
const configApi = useApi(configApiRef);
const clusters: Config[] = configApi.getConfigArray('kubernetes.clusters');
const allAuthProviders: string[] = clusters.map(c =>
c.getString('authProvider'),
);
const authProviders: string[] = [...new Set(allAuthProviders)];
const kubernetesAuthProvidersApi = useApi(kubernetesAuthProvidersApiRef);
useEffect(() => {
kubernetesApi
.getObjectsByServiceId(entity.metadata.name)
.then(result => {
setKubernetesObjects(result);
})
.catch(e => {
setError(e.message);
});
}, [entity.metadata.name, kubernetesApi]);
(async () => {
// For each auth type, invoke decorateRequestBodyForAuth on corresponding KubernetesAuthProvider
let requestBody: AuthRequestBody = {};
for (const authProviderStr of authProviders) {
// Multiple asyncs done sequentially instead of all at once to prevent same requestBody from being modified simultaneously
requestBody = await kubernetesAuthProvidersApi.decorateRequestBodyForAuth(
authProviderStr,
requestBody,
);
}
// TODO: Add validation on contents/format of requestBody
kubernetesApi
.getObjectsByServiceId(entity.metadata.name, requestBody)
.then(result => {
setKubernetesObjects(result);
})
.catch(e => {
setError(e.message);
});
})();
/* eslint-disable react-hooks/exhaustive-deps */
}, [entity.metadata.name, kubernetesApi, kubernetesAuthProvidersApi]);
/* eslint-enable react-hooks/exhaustive-deps */
const clustersWithErrors =
kubernetesObjects?.items.filter(r => r.errors.length > 0) ?? [];
@@ -0,0 +1,41 @@
/*
* Copyright 2020 Spotify AB
*
* 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 } from '@backstage/core';
import { KubernetesAuthProvider } from './types';
import { AuthRequestBody } from '@backstage/plugin-kubernetes-backend';
export class GoogleKubernetesAuthProvider implements KubernetesAuthProvider {
authProvider: OAuthApi;
constructor(authProvider: OAuthApi) {
this.authProvider = authProvider;
}
async decorateRequestBodyForAuth(
requestBody: AuthRequestBody,
): Promise<AuthRequestBody> {
const googleAuthToken: string = await this.authProvider.getAccessToken(
'https://www.googleapis.com/auth/cloud-platform',
);
if ('auth' in requestBody) {
requestBody.auth!.google = googleAuthToken;
} else {
requestBody.auth = { google: googleAuthToken };
}
return requestBody;
}
}
@@ -0,0 +1,57 @@
/*
* Copyright 2020 Spotify AB
*
* 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 } from '@backstage/core';
import { AuthRequestBody } from '@backstage/plugin-kubernetes-backend';
import { KubernetesAuthProvider, KubernetesAuthProvidersApi } from './types';
import { GoogleKubernetesAuthProvider } from './GoogleKubernetesAuthProvider';
import { ServiceAccountKubernetesAuthProvider } from './ServiceAccountKubernetesAuthProvider';
export class KubernetesAuthProviders implements KubernetesAuthProvidersApi {
private readonly kubernetesAuthProviderMap: Map<
string,
KubernetesAuthProvider
>;
constructor(options: { googleAuthApi: OAuthApi }) {
this.kubernetesAuthProviderMap = new Map<string, KubernetesAuthProvider>();
this.kubernetesAuthProviderMap.set(
'google',
new GoogleKubernetesAuthProvider(options.googleAuthApi),
);
this.kubernetesAuthProviderMap.set(
'serviceAccount',
new ServiceAccountKubernetesAuthProvider(),
);
}
async decorateRequestBodyForAuth(
authProvider: string,
requestBody: AuthRequestBody,
): Promise<AuthRequestBody> {
const kubernetesAuthProvider:
| KubernetesAuthProvider
| undefined = this.kubernetesAuthProviderMap.get(authProvider);
if (kubernetesAuthProvider) {
return await kubernetesAuthProvider.decorateRequestBodyForAuth(
requestBody,
);
}
throw new Error(
`authProvider "${authProvider}" has no KubernetesAuthProvider defined for it`,
);
}
}
@@ -0,0 +1,28 @@
/*
* Copyright 2020 Spotify AB
*
* 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 { AuthRequestBody } from '@backstage/plugin-kubernetes-backend';
export class ServiceAccountKubernetesAuthProvider
implements KubernetesAuthProvider {
async decorateRequestBodyForAuth(
requestBody: AuthRequestBody,
): Promise<AuthRequestBody> {
// No-op, with service account for auth, cluster config/details should already have serviceAccountToken
return requestBody;
}
}
@@ -0,0 +1,38 @@
/*
* Copyright 2020 Spotify AB
*
* 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 { createApiRef } from '@backstage/core';
import { AuthRequestBody } from '@backstage/plugin-kubernetes-backend';
export interface KubernetesAuthProvider {
decorateRequestBodyForAuth(
requestBody: AuthRequestBody,
): Promise<AuthRequestBody>;
}
export const kubernetesAuthProvidersApiRef = createApiRef<
KubernetesAuthProvidersApi
>({
id: 'plugin.kubernetes-auth-providers.service',
description: 'Used by the Kubernetes plugin to fetch KubernetesAuthProviders',
});
export interface KubernetesAuthProvidersApi {
decorateRequestBodyForAuth(
authProvider: string,
requestBody: AuthRequestBody,
): Promise<AuthRequestBody>;
}
+10
View File
@@ -18,9 +18,12 @@ import {
createPlugin,
createRouteRef,
discoveryApiRef,
googleAuthApiRef,
} from '@backstage/core';
import { KubernetesBackendClient } from './api/KubernetesBackendClient';
import { kubernetesApiRef } from './api/types';
import { kubernetesAuthProvidersApiRef } from './kubernetes-auth-provider/types';
import { KubernetesAuthProviders } from './kubernetes-auth-provider/KubernetesAuthProviders';
export const rootCatalogKubernetesRouteRef = createRouteRef({
path: '*',
@@ -36,5 +39,12 @@ export const plugin = createPlugin({
factory: ({ discoveryApi }) =>
new KubernetesBackendClient({ discoveryApi }),
}),
createApiFactory({
api: kubernetesAuthProvidersApiRef,
deps: { googleAuthApi: googleAuthApiRef },
factory: ({ googleAuthApi }) => {
return new KubernetesAuthProviders({ googleAuthApi });
},
}),
],
});