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
+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 });
},
}),
],
});