diff --git a/.changeset/brown-keys-raise.md b/.changeset/brown-keys-raise.md new file mode 100644 index 0000000000..1d288d9f57 --- /dev/null +++ b/.changeset/brown-keys-raise.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-kubernetes': patch +--- + +The Kubernetes plugin now requests AKS access tokens from Azure when retrieving +objects from clusters configured with `authProvider: aks` and sets `auth.aks` in +its request bodies appropriately. diff --git a/.changeset/fluffy-mirrors-happen.md b/.changeset/fluffy-mirrors-happen.md new file mode 100644 index 0000000000..6dce398217 --- /dev/null +++ b/.changeset/fluffy-mirrors-happen.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-kubernetes-backend': patch +--- + +Kubernetes clusters now support `authProvider: aks`. When configured this way, +the `retrieveObjectsByServiceId` action will use the `auth.aks` value in the +request body as a bearer token to authenticate with Kubernetes. diff --git a/.changeset/thirty-jobs-sort.md b/.changeset/thirty-jobs-sort.md new file mode 100644 index 0000000000..8a40302502 --- /dev/null +++ b/.changeset/thirty-jobs-sort.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes-common': patch +--- + +AKS access tokens can now be sent over the wire to the Kubernetes backend. diff --git a/docs/features/kubernetes/authentication.md b/docs/features/kubernetes/authentication.md index d44d3a4a64..ddeabb5db0 100644 --- a/docs/features/kubernetes/authentication.md +++ b/docs/features/kubernetes/authentication.md @@ -66,6 +66,7 @@ as `401` or similar. The providers available as client side are: +- `aks` - `google` - `oidc` diff --git a/docs/features/kubernetes/configuration.md b/docs/features/kubernetes/configuration.md index 458637f316..9280c4f020 100644 --- a/docs/features/kubernetes/configuration.md +++ b/docs/features/kubernetes/configuration.md @@ -104,12 +104,13 @@ cluster. Valid values are: | Value | Description | | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `serviceAccount` | This will use a Kubernetes [service account](https://kubernetes.io/docs/reference/access-authn-authz/service-accounts-admin/) to access the Kubernetes API. When this is used the `serviceAccountToken` field should also be set. | -| `google` | This will use a user's Google auth token from the [Google auth plugin](https://backstage.io/docs/auth/) to access the Kubernetes API. | +| `aks` | This will use a user's AKS access token from the [Microsoft auth provider](https://backstage.io/docs/auth/microsoft/provider) to access the Kubernetes API on AKS clusters. | | `aws` | This will use AWS credentials to access resources in EKS clusters | -| `googleServiceAccount` | This will use the Google Cloud service account credentials to access resources in clusters | | `azure` | This will use [Azure Identity](https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview) to access resources in clusters | +| `google` | This will use a user's Google access token from the [Google auth provider](https://backstage.io/docs/auth/google/provider) to access the Kubernetes API on GKE clusters. | +| `googleServiceAccount` | This will use the Google Cloud service account credentials to access resources in clusters | | `oidc` | This will use [Oidc Tokens](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens) to authenticate to the Kubernetes API. When this is used the `oidcTokenProvider` field should also be set. Please note the cluster must support OIDC, at the time of writing AKS clusters do not support OIDC. | +| `serviceAccount` | This will use a Kubernetes [service account](https://kubernetes.io/docs/reference/access-authn-authz/service-accounts-admin/) to access the Kubernetes API. When this is used the `serviceAccountToken` field should also be set. | Check the [Kubernetes Authentication][4] section for additional explanation. diff --git a/plugins/kubernetes-backend/api-report.md b/plugins/kubernetes-backend/api-report.md index 668061cd31..ad8ff32891 100644 --- a/plugins/kubernetes-backend/api-report.md +++ b/plugins/kubernetes-backend/api-report.md @@ -21,6 +21,15 @@ import { PluginEndpointDiscovery } from '@backstage/backend-common'; import type { RequestHandler } from 'express'; import { TokenCredential } from '@azure/identity'; +// @public (undocumented) +export class AksKubernetesAuthTranslator { + // (undocumented) + decorateClusterDetailsWithAuth( + clusterDetails: ClusterDetails, + auth: KubernetesRequestAuth, + ): Promise; +} + // @public (undocumented) export interface AWSClusterDetails extends ClusterDetails { // (undocumented) diff --git a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts index 5d8fefc5d2..7f545a95de 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts @@ -256,4 +256,19 @@ describe('ConfigClusterLocator', () => { }, ]); }); + + it('supports aks authProvider', async () => { + const cluster = { + name: 'aks-cluster', + url: 'https://aks.test', + authProvider: 'aks', + }; + const sut = ConfigClusterLocator.fromConfig( + new ConfigReader({ clusters: [cluster] }), + ); + + const result = await sut.getClusters(); + + expect(result).toMatchObject([cluster]); + }); }); diff --git a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts index b0143c2010..a759cb4bb0 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts @@ -76,6 +76,9 @@ export class ConfigClusterLocator implements KubernetesClustersSupplier { case 'googleServiceAccount': { return clusterDetails; } + case 'aks': { + return clusterDetails; + } default: { throw new Error( `authProvider "${authProvider}" has no config associated with it`, diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AksKubernetesAuthTranslator.test.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AksKubernetesAuthTranslator.test.ts new file mode 100644 index 0000000000..d0d3cf87bb --- /dev/null +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AksKubernetesAuthTranslator.test.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2023 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 { AksKubernetesAuthTranslator } from './AksKubernetesAuthTranslator'; + +describe('AksKubernetesAuthTranslator', () => { + it('uses auth.aks value as bearer token', async () => { + const translator = new AksKubernetesAuthTranslator(); + + const details = await translator.decorateClusterDetailsWithAuth( + { name: '', authProvider: 'aks', url: '' }, + { aks: 'aksToken' }, + ); + + expect(details.serviceAccountToken).toBe('aksToken'); + }); +}); diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AksKubernetesAuthTranslator.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AksKubernetesAuthTranslator.ts new file mode 100644 index 0000000000..13d784293e --- /dev/null +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AksKubernetesAuthTranslator.ts @@ -0,0 +1,30 @@ +/* + * Copyright 2023 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 { ClusterDetails } from '../types/types'; +import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common'; + +/** + * + * @public + */ +export class AksKubernetesAuthTranslator { + async decorateClusterDetailsWithAuth( + clusterDetails: ClusterDetails, + auth: KubernetesRequestAuth, + ): Promise { + return { ...clusterDetails, serviceAccountToken: auth.aks }; + } +} diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/index.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/index.ts index 4ee83b0da0..71dcedfee6 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/index.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/index.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +export * from './AksKubernetesAuthTranslator'; export * from './AwsIamKubernetesAuthTranslator'; export * from './AzureIdentityKubernetesAuthTranslator'; export * from './GoogleKubernetesAuthTranslator'; diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts index 75b619b412..8f9d0769ed 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts @@ -33,6 +33,7 @@ import { GoogleServiceAccountAuthTranslator, AzureIdentityKubernetesAuthTranslator, OidcKubernetesAuthTranslator, + AksKubernetesAuthTranslator, } from '../kubernetes-auth-translator'; import { addResourceRoutesToRouter } from '../routes/resourcesRoutes'; @@ -355,6 +356,7 @@ export class KubernetesBuilder { protected buildAuthTranslatorMap() { this.authTranslatorMap = { google: new GoogleKubernetesAuthTranslator(), + aks: new AksKubernetesAuthTranslator(), aws: new AwsIamKubernetesAuthTranslator({ config: this.env.config }), azure: new AzureIdentityKubernetesAuthTranslator(this.env.logger), serviceAccount: new NoopKubernetesAuthTranslator(), diff --git a/plugins/kubernetes-common/api-report.md b/plugins/kubernetes-common/api-report.md index 6c88074e50..56faeea7d9 100644 --- a/plugins/kubernetes-common/api-report.md +++ b/plugins/kubernetes-common/api-report.md @@ -226,6 +226,8 @@ export const kubernetesProxyPermission: BasicPermission; // @public (undocumented) export interface KubernetesRequestAuth { + // (undocumented) + aks?: string; // (undocumented) google?: string; // (undocumented) diff --git a/plugins/kubernetes-common/src/types.ts b/plugins/kubernetes-common/src/types.ts index f5701d49fe..8eabb16d24 100644 --- a/plugins/kubernetes-common/src/types.ts +++ b/plugins/kubernetes-common/src/types.ts @@ -35,6 +35,7 @@ import { Entity } from '@backstage/catalog-model'; /** @public */ export interface KubernetesRequestAuth { google?: string; + aks?: string; oidc?: { [key: string]: string; }; diff --git a/plugins/kubernetes/api-report.md b/plugins/kubernetes/api-report.md index 60f5d3fc99..5529b7910f 100644 --- a/plugins/kubernetes/api-report.md +++ b/plugins/kubernetes/api-report.md @@ -281,6 +281,7 @@ export const kubernetesApiRef: ApiRef; // @public (undocumented) export class KubernetesAuthProviders implements KubernetesAuthProvidersApi { constructor(options: { + microsoftAuthApi: OAuthApi; googleAuthApi: OAuthApi; oidcProviders?: { [key: string]: OpenIdConnectApi; diff --git a/plugins/kubernetes/src/kubernetes-auth-provider/AksKubernetesAuthProvider.ts b/plugins/kubernetes/src/kubernetes-auth-provider/AksKubernetesAuthProvider.ts new file mode 100644 index 0000000000..401fc1e127 --- /dev/null +++ b/plugins/kubernetes/src/kubernetes-auth-provider/AksKubernetesAuthProvider.ts @@ -0,0 +1,39 @@ +/* + * Copyright 2023 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 } from '@backstage/core-plugin-api'; +import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common'; +import { KubernetesAuthProvider } from './types'; + +export class AksKubernetesAuthProvider implements KubernetesAuthProvider { + constructor(private readonly microsoftAuthApi: OAuthApi) {} + + async decorateRequestBodyForAuth( + requestBody: KubernetesRequestBody, + ): Promise { + return { + ...requestBody, + auth: { ...requestBody.auth, aks: (await this.getCredentials()).token }, + }; + } + + async getCredentials(): Promise<{ token?: string }> { + return { + token: await this.microsoftAuthApi.getAccessToken( + '6dae42f8-4368-4678-94ff-3960e28e3630/user.read', + ), + }; + } +} diff --git a/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.test.ts b/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.test.ts index a83cbcfaa4..14e574022a 100644 --- a/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.test.ts +++ b/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.test.ts @@ -39,11 +39,18 @@ const requestBody: KubernetesRequestBody = { }; describe('KubernetesAuthProviders tests', () => { - const kap = new KubernetesAuthProviders({ - googleAuthApi: new MockAuthApi('googleToken'), - oidcProviders: { - okta: new MockAuthApi('oktaToken'), - }, + let microsoftAuthApi: MockAuthApi; + let kap: KubernetesAuthProviders; + + beforeEach(() => { + microsoftAuthApi = new MockAuthApi('aksToken'); + kap = new KubernetesAuthProviders({ + microsoftAuthApi, + googleAuthApi: new MockAuthApi('googleToken'), + oidcProviders: { + okta: new MockAuthApi('oktaToken'), + }, + }); }); it('adds token to request body for google authProvider', async () => { @@ -52,6 +59,15 @@ describe('KubernetesAuthProviders tests', () => { expect(details.auth?.google).toBe('googleToken'); }); + it('adds token to request body for aks authProvider', async () => { + const details = await kap.decorateRequestBodyForAuth('aks', requestBody); + + expect(details.auth?.aks).toBe('aksToken'); + expect(microsoftAuthApi.getAccessToken).toHaveBeenCalledWith( + '6dae42f8-4368-4678-94ff-3960e28e3630/user.read', + ); + }); + it('adds token to request body for oidc authProvider', async () => { const details = await kap.decorateRequestBodyForAuth( 'oidc.okta', diff --git a/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.ts b/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.ts index 836641f1ed..84154d5e96 100644 --- a/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.ts +++ b/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.ts @@ -20,6 +20,7 @@ import { GoogleKubernetesAuthProvider } from './GoogleKubernetesAuthProvider'; import { ServerSideKubernetesAuthProvider } from './ServerSideAuthProvider'; import { OAuthApi, OpenIdConnectApi } from '@backstage/core-plugin-api'; import { OidcKubernetesAuthProvider } from './OidcKubernetesAuthProvider'; +import { AksKubernetesAuthProvider } from './AksKubernetesAuthProvider'; export class KubernetesAuthProviders implements KubernetesAuthProvidersApi { private readonly kubernetesAuthProviderMap: Map< @@ -28,6 +29,7 @@ export class KubernetesAuthProviders implements KubernetesAuthProvidersApi { >; constructor(options: { + microsoftAuthApi: OAuthApi; googleAuthApi: OAuthApi; oidcProviders?: { [key: string]: OpenIdConnectApi }; }) { @@ -56,6 +58,10 @@ export class KubernetesAuthProviders implements KubernetesAuthProvidersApi { 'localKubectlProxy', new ServerSideKubernetesAuthProvider(), ); + this.kubernetesAuthProviderMap.set( + 'aks', + new AksKubernetesAuthProvider(options.microsoftAuthApi), + ); if (options.oidcProviders) { Object.keys(options.oidcProviders).forEach(provider => { diff --git a/plugins/kubernetes/src/plugin.ts b/plugins/kubernetes/src/plugin.ts index e10b4856b7..02c7bfd2d6 100644 --- a/plugins/kubernetes/src/plugin.ts +++ b/plugins/kubernetes/src/plugin.ts @@ -87,7 +87,11 @@ export const kubernetesPlugin = createPlugin({ onelogin: oneloginAuthApi, }; - return new KubernetesAuthProviders({ googleAuthApi, oidcProviders }); + return new KubernetesAuthProviders({ + microsoftAuthApi, + googleAuthApi, + oidcProviders, + }); }, }), ],