integrators can add strategies

Due to the loosened wire format, now integrators have the opportunity to define
arbitrary contracts between the front- and back-end when dealing with customized
Kubernetes auth setups.

Signed-off-by: Jamie Klassen <jklassen@vmware.com>
This commit is contained in:
Jamie Klassen
2023-09-11 17:07:11 -04:00
parent e30bd8709d
commit 5d377c9b39
12 changed files with 149 additions and 31 deletions
@@ -27,7 +27,9 @@ export class AksStrategy implements AuthenticationStrategy {
requestAuth: KubernetesRequestAuth,
): Promise<KubernetesCredential> {
const token = requestAuth.aks;
return token ? { type: 'bearer token', token } : { type: 'anonymous' };
return token
? { type: 'bearer token', token: token as string }
: { type: 'anonymous' };
}
public validate(_: AuthMetadata) {}
}
@@ -33,7 +33,7 @@ export class GoogleStrategy implements AuthenticationStrategy {
'Google token not found under auth.google in request body',
);
}
return { type: 'bearer token', token };
return { type: 'bearer token', token: token as string };
}
public validate(_: AuthMetadata) {}
}
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { JsonObject } from '@backstage/types';
import {
ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER,
KubernetesRequestAuth,
@@ -38,14 +39,14 @@ export class OidcStrategy implements AuthenticationStrategy {
);
}
const token = authConfig.oidc?.[oidcTokenProvider];
const token = (authConfig.oidc as JsonObject | null)?.[oidcTokenProvider];
if (!token) {
throw new Error(
`Auth token not found under oidc.${oidcTokenProvider} in request body`,
);
}
return { type: 'bearer token', token };
return { type: 'bearer token', token: token as string };
}
public validate(authMetadata: AuthMetadata) {
@@ -21,6 +21,7 @@ import {
ANNOTATION_KUBERNETES_AUTH_PROVIDER,
ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER,
ObjectsByEntityResponse,
KubernetesRequestAuth,
} from '@backstage/plugin-kubernetes-common';
import express from 'express';
import request from 'supertest';
@@ -55,7 +56,8 @@ describe('KubernetesBuilder', () => {
let catalogApi: CatalogApi;
let permissions: jest.Mocked<PermissionEvaluator>;
beforeAll(async () => {
beforeEach(async () => {
jest.resetAllMocks();
const logger = getVoidLogger();
config = new ConfigReader({
kubernetes: {
@@ -109,10 +111,6 @@ describe('KubernetesBuilder', () => {
app = express().use(router);
});
beforeEach(() => {
jest.resetAllMocks();
});
describe('get /clusters', () => {
it('happy path: lists clusters', async () => {
const response = await request(app).get('/clusters');
@@ -298,8 +296,77 @@ describe('KubernetesBuilder', () => {
expect(response.body).toEqual(result);
expect(response.status).toEqual(200);
});
it('reads auth data for custom strategy', async () => {
permissions.authorize.mockResolvedValue([
{ result: AuthorizeResult.ALLOW },
]);
const mockFetcher = {
fetchPodMetricsByNamespaces: jest
.fn()
.mockResolvedValue({ errors: [], responses: [] }),
fetchObjectsForService: jest.fn().mockResolvedValue({
errors: [],
responses: [
{ type: 'pods', resources: [{ metadata: { name: 'pod1' } }] },
],
}),
};
const { router } = await KubernetesBuilder.createBuilder({
logger: getVoidLogger(),
config,
catalogApi,
permissions,
})
.addAuthStrategy('custom', {
getCredential: jest
.fn<
Promise<KubernetesCredential>,
[ClusterDetails, KubernetesRequestAuth]
>()
.mockImplementation(async (_, requestAuth) => ({
type: 'bearer token',
token: requestAuth.custom as string,
})),
validate: jest.fn(),
})
.setClusterSupplier({
getClusters: jest
.fn<Promise<ClusterDetails[]>, []>()
.mockResolvedValue([
{
name: 'custom-cluster',
url: 'http://my.cluster.url',
authMetadata: {
[ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'custom',
},
},
]),
})
.setFetcher(mockFetcher)
.build();
app = express().use(router);
await request(app)
.post('/services/test-service')
.send({
entity: {
metadata: {
name: 'thing',
},
},
auth: { custom: 'custom-token' },
});
expect(mockFetcher.fetchObjectsForService).toHaveBeenCalledWith(
expect.objectContaining({
credential: { type: 'bearer token', token: 'custom-token' },
}),
);
});
});
describe('post /proxy', () => {
describe('/proxy', () => {
const worker = setupServer();
setupRequestMockHandlers(worker);
@@ -400,6 +467,59 @@ metadata:
expect(response.status).toEqual(403);
});
it('permits custom client-side auth strategy', async () => {
worker.use(
rest.get('http://my.cluster.url/api/v1/namespaces', (req, res, ctx) => {
if (req.headers.get('Authorization') !== 'custom-token') {
return res(ctx.status(401));
}
return res(ctx.json({ items: [] }));
}),
);
permissions.authorize.mockResolvedValue([
{ result: AuthorizeResult.ALLOW },
]);
const { router } = await KubernetesBuilder.createBuilder({
logger: getVoidLogger(),
config,
catalogApi,
permissions,
})
.addAuthStrategy('custom', {
getCredential: jest
.fn<
Promise<KubernetesCredential>,
[ClusterDetails, KubernetesRequestAuth]
>()
.mockResolvedValue({ type: 'anonymous' }),
validate: jest.fn(),
})
.setClusterSupplier({
getClusters: jest
.fn<Promise<ClusterDetails[]>, []>()
.mockResolvedValue([
{
name: 'custom-cluster',
url: 'http://my.cluster.url',
authMetadata: {
[ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'custom',
},
},
]),
})
.build();
app = express().use(router);
const proxyEndpointRequest = request(app)
.get('/proxy/api/v1/namespaces')
.set(HEADER_KUBERNETES_CLUSTER, 'custom-cluster')
.set(HEADER_KUBERNETES_AUTH, 'custom-token');
worker.use(rest.all(proxyEndpointRequest.url, req => req.passthrough()));
const response = await proxyEndpointRequest;
expect(response.body).toStrictEqual({ items: [] });
});
});
describe('get /.well-known/backstage/permissions/metadata', () => {
it('lists permissions supported by the kubernetes plugin', async () => {
@@ -204,6 +204,11 @@ export class KubernetesBuilder {
this.authStrategyMap = authStrategyMap;
}
public addAuthStrategy(key: string, strategy: AuthenticationStrategy) {
this.getAuthStrategyMap()[key] = strategy;
return this;
}
protected buildCustomResources() {
const customResources: CustomResource[] = (
this.env.config.getOptionalConfigArray('kubernetes.customResources') ?? []