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:
@@ -166,6 +166,8 @@ export const HEADER_KUBERNETES_CLUSTER: string;
|
||||
export class KubernetesBuilder {
|
||||
constructor(env: KubernetesEnvironment);
|
||||
// (undocumented)
|
||||
addAuthStrategy(key: string, strategy: AuthenticationStrategy): this;
|
||||
// (undocumented)
|
||||
build(): KubernetesBuilderReturn;
|
||||
// (undocumented)
|
||||
protected buildAuthStrategyMap(): {
|
||||
|
||||
@@ -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') ?? []
|
||||
|
||||
@@ -233,16 +233,7 @@ export const kubernetesPermissions: BasicPermission[];
|
||||
export const kubernetesProxyPermission: BasicPermission;
|
||||
|
||||
// @public (undocumented)
|
||||
export interface KubernetesRequestAuth {
|
||||
// (undocumented)
|
||||
aks?: string;
|
||||
// (undocumented)
|
||||
google?: string;
|
||||
// (undocumented)
|
||||
oidc?: {
|
||||
[key: string]: string;
|
||||
};
|
||||
}
|
||||
export type KubernetesRequestAuth = JsonObject;
|
||||
|
||||
// @public (undocumented)
|
||||
export interface KubernetesRequestBody {
|
||||
|
||||
@@ -33,13 +33,7 @@ import {
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
|
||||
/** @public */
|
||||
export interface KubernetesRequestAuth {
|
||||
google?: string;
|
||||
aks?: string;
|
||||
oidc?: {
|
||||
[key: string]: string;
|
||||
};
|
||||
}
|
||||
export type KubernetesRequestAuth = JsonObject;
|
||||
|
||||
/** @public */
|
||||
export interface CustomResourceMatcher {
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
"@backstage/plugin-catalog-react": "workspace:^",
|
||||
"@backstage/plugin-kubernetes-common": "workspace:^",
|
||||
"@backstage/theme": "workspace:^",
|
||||
"@backstage/types": "workspace:^",
|
||||
"@kubernetes-models/apimachinery": "^1.1.0",
|
||||
"@kubernetes-models/base": "^4.0.1",
|
||||
"@kubernetes/client-node": "0.18.1",
|
||||
|
||||
@@ -74,7 +74,7 @@ describe('KubernetesAuthProviders tests', () => {
|
||||
requestBody,
|
||||
);
|
||||
|
||||
expect(details.auth?.oidc?.okta).toBe('oktaToken');
|
||||
expect(details).toMatchObject({ auth: { oidc: { okta: 'oktaToken' } } });
|
||||
});
|
||||
|
||||
it('returns error for unknown authProvider', async () => {
|
||||
|
||||
@@ -14,9 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { KubernetesAuthProvider } from './types';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common';
|
||||
import { OpenIdConnectApi } from '@backstage/core-plugin-api';
|
||||
import { KubernetesAuthProvider } from './types';
|
||||
|
||||
export class OidcKubernetesAuthProvider implements KubernetesAuthProvider {
|
||||
providerName: string;
|
||||
@@ -31,9 +32,9 @@ export class OidcKubernetesAuthProvider implements KubernetesAuthProvider {
|
||||
requestBody: KubernetesRequestBody,
|
||||
): Promise<KubernetesRequestBody> {
|
||||
const authToken: string = (await this.getCredentials()).token;
|
||||
const auth = { ...requestBody.auth };
|
||||
const auth = { ...(requestBody.auth as JsonObject) };
|
||||
if (auth.oidc) {
|
||||
auth.oidc[this.providerName] = authToken;
|
||||
(auth.oidc as JsonObject)[this.providerName] = authToken;
|
||||
} else {
|
||||
auth.oidc = { [this.providerName]: authToken };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user