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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+28
@@ -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>;
|
||||
}
|
||||
@@ -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 });
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user