Introduce kubernetes resource permission
Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-kubernetes-backend': minor
|
||||
'@backstage/plugin-kubernetes-common': minor
|
||||
---
|
||||
|
||||
Introduced resource permission type to be used with the kubernetes endpoint's permission framework integration for all endpoints except the proxy endpoints.
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2024 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 type {
|
||||
HttpAuthService,
|
||||
PermissionsService,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { NotAllowedError } from '@backstage/errors';
|
||||
import { kubernetesResourcePermission } from '@backstage/plugin-kubernetes-common';
|
||||
import { AuthorizeResult } from '@backstage/plugin-permission-common';
|
||||
|
||||
import express from 'express';
|
||||
|
||||
export async function requirePermission(
|
||||
permissionApi: PermissionsService,
|
||||
httpAuth: HttpAuthService,
|
||||
req: express.Request,
|
||||
) {
|
||||
const decision = (
|
||||
await permissionApi.authorize(
|
||||
[
|
||||
{
|
||||
permission: kubernetesResourcePermission,
|
||||
},
|
||||
],
|
||||
{
|
||||
credentials: await httpAuth.credentials(req),
|
||||
},
|
||||
)
|
||||
)[0];
|
||||
|
||||
if (decision.result === AuthorizeResult.DENY) {
|
||||
const err = new NotAllowedError('Unauthorized');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
@@ -18,17 +18,48 @@ import request from 'supertest';
|
||||
import {
|
||||
mockCredentials,
|
||||
mockServices,
|
||||
type ServiceMock,
|
||||
startTestBackend,
|
||||
} from '@backstage/backend-test-utils';
|
||||
import { kubernetesObjectsProviderExtensionPoint } from '@backstage/plugin-kubernetes-node';
|
||||
import { createBackendModule } from '@backstage/backend-plugin-api';
|
||||
import {
|
||||
createBackendModule,
|
||||
type PermissionsService,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { ExtendedHttpServer } from '@backstage/backend-defaults/rootHttpRouter';
|
||||
import { AuthorizeResult } from '@backstage/plugin-permission-common';
|
||||
|
||||
describe('resourcesRoutes', () => {
|
||||
let app: ExtendedHttpServer;
|
||||
const permissionsMock: ServiceMock<PermissionsService> =
|
||||
mockServices.permissions.mock({
|
||||
authorize: jest.fn(),
|
||||
authorizeConditional: jest.fn(),
|
||||
});
|
||||
|
||||
beforeAll(async () => {
|
||||
const startPermissionDeniedTestServer = async () => {
|
||||
permissionsMock.authorize.mockResolvedValue([
|
||||
{ result: AuthorizeResult.DENY },
|
||||
]);
|
||||
const { server } = await startTestBackend({
|
||||
features: [
|
||||
mockServices.rootConfig.factory({
|
||||
data: {
|
||||
kubernetes: {
|
||||
serviceLocatorMethod: { type: 'multiTenant' },
|
||||
clusterLocatorMethods: [],
|
||||
},
|
||||
},
|
||||
}),
|
||||
permissionsMock.factory,
|
||||
import('@backstage/plugin-kubernetes-backend'),
|
||||
],
|
||||
});
|
||||
return server;
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const objectsProviderMock = {
|
||||
getKubernetesObjectsByEntity: jest.fn().mockImplementation(args => {
|
||||
if (args.entity.metadata.name === 'inject500') {
|
||||
@@ -109,6 +140,8 @@ describe('resourcesRoutes', () => {
|
||||
},
|
||||
}),
|
||||
import('@backstage/plugin-kubernetes-backend'),
|
||||
import('@backstage/plugin-permission-backend'),
|
||||
import('@backstage/plugin-permission-backend-module-allow-all-policy'),
|
||||
createBackendModule({
|
||||
pluginId: 'kubernetes',
|
||||
moduleId: 'test-objects-provider',
|
||||
@@ -127,6 +160,10 @@ describe('resourcesRoutes', () => {
|
||||
app = server;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
app.stop();
|
||||
});
|
||||
|
||||
describe('POST /resources/workloads/query', () => {
|
||||
// eslint-disable-next-line jest/expect-expect
|
||||
it('200 happy path', async () => {
|
||||
@@ -269,6 +306,13 @@ describe('resourcesRoutes', () => {
|
||||
response: { statusCode: 401 },
|
||||
});
|
||||
});
|
||||
it('403 when permission blocks endpoint', async () => {
|
||||
app = await startPermissionDeniedTestServer();
|
||||
const response = await request(app).post(
|
||||
'/api/kubernetes/resources/workloads/query',
|
||||
);
|
||||
expect(response.status).toEqual(403);
|
||||
});
|
||||
// eslint-disable-next-line jest/expect-expect
|
||||
it('500 handle gracefully', async () => {
|
||||
await request(app)
|
||||
@@ -548,6 +592,13 @@ describe('resourcesRoutes', () => {
|
||||
response: { statusCode: 401 },
|
||||
});
|
||||
});
|
||||
it('403 when permission blocks endpoint', async () => {
|
||||
app = await startPermissionDeniedTestServer();
|
||||
const response = await request(app).post(
|
||||
'/api/kubernetes/resources/custom/query',
|
||||
);
|
||||
expect(response.status).toEqual(403);
|
||||
});
|
||||
// eslint-disable-next-line jest/expect-expect
|
||||
it('500 handle gracefully', async () => {
|
||||
await request(app)
|
||||
|
||||
@@ -23,6 +23,8 @@ import { InputError } from '@backstage/errors';
|
||||
import express, { Request } from 'express';
|
||||
import { KubernetesObjectsProvider } from '@backstage/plugin-kubernetes-node';
|
||||
import { AuthService, HttpAuthService } from '@backstage/backend-plugin-api';
|
||||
import { PermissionEvaluator } from '@backstage/plugin-permission-common';
|
||||
import { requirePermission } from '../auth/requirePermission';
|
||||
|
||||
export const addResourceRoutesToRouter = (
|
||||
router: express.Router,
|
||||
@@ -30,6 +32,7 @@ export const addResourceRoutesToRouter = (
|
||||
objectsProvider: KubernetesObjectsProvider,
|
||||
auth: AuthService,
|
||||
httpAuth: HttpAuthService,
|
||||
permissionApi: PermissionEvaluator,
|
||||
) => {
|
||||
const getEntityByReq = async (req: Request<any>) => {
|
||||
const rawEntityRef = req.body.entityRef;
|
||||
@@ -62,6 +65,7 @@ export const addResourceRoutesToRouter = (
|
||||
};
|
||||
|
||||
router.post('/resources/workloads/query', async (req, res) => {
|
||||
await requirePermission(permissionApi, httpAuth, req);
|
||||
const entity = await getEntityByReq(req);
|
||||
const response = await objectsProvider.getKubernetesObjectsByEntity(
|
||||
{
|
||||
@@ -74,6 +78,7 @@ export const addResourceRoutesToRouter = (
|
||||
});
|
||||
|
||||
router.post('/resources/custom/query', async (req, res) => {
|
||||
await requirePermission(permissionApi, httpAuth, req);
|
||||
const entity = await getEntityByReq(req);
|
||||
|
||||
if (!req.body.customResources) {
|
||||
|
||||
@@ -92,6 +92,19 @@ describe('API integration tests', () => {
|
||||
});
|
||||
},
|
||||
});
|
||||
const startPermissionDeniedTestServer = async () => {
|
||||
permissionsMock.authorize.mockResolvedValue([
|
||||
{ result: AuthorizeResult.DENY },
|
||||
]);
|
||||
const { server } = await startTestBackend({
|
||||
features: [
|
||||
minimalValidConfigService,
|
||||
permissionsMock.factory,
|
||||
import('@backstage/plugin-kubernetes-backend'),
|
||||
],
|
||||
});
|
||||
return server;
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.resetAllMocks();
|
||||
@@ -305,6 +318,12 @@ describe('API integration tests', () => {
|
||||
items: [expect.objectContaining({ title: 'cluster-title' })],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns 403 response when permission blocks endpoint', async () => {
|
||||
app = await startPermissionDeniedTestServer();
|
||||
const response = await request(app).get('/api/kubernetes/clusters');
|
||||
expect(response.status).toEqual(403);
|
||||
});
|
||||
});
|
||||
|
||||
describe('post /services/:serviceId', () => {
|
||||
@@ -504,6 +523,14 @@ describe('API integration tests', () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('returns 403 response when permission blocks endpoint', async () => {
|
||||
app = await startPermissionDeniedTestServer();
|
||||
const response = await request(app).post(
|
||||
'/api/kubernetes/services/test-service',
|
||||
);
|
||||
expect(response.status).toEqual(403);
|
||||
});
|
||||
});
|
||||
|
||||
describe('/proxy', () => {
|
||||
@@ -571,18 +598,7 @@ metadata:
|
||||
});
|
||||
|
||||
it('returns 403 response when permission blocks endpoint', async () => {
|
||||
permissionsMock.authorize.mockResolvedValue([
|
||||
{ result: AuthorizeResult.DENY },
|
||||
]);
|
||||
|
||||
const { server } = await startTestBackend({
|
||||
features: [
|
||||
minimalValidConfigService,
|
||||
permissionsMock.factory,
|
||||
import('@backstage/plugin-kubernetes-backend'),
|
||||
],
|
||||
});
|
||||
app = server;
|
||||
app = await startPermissionDeniedTestServer();
|
||||
|
||||
const proxyEndpointRequest = request(app)
|
||||
.post('/api/kubernetes/proxy/api/v1/namespaces')
|
||||
@@ -779,6 +795,7 @@ metadata:
|
||||
expect(response.body).toMatchObject({
|
||||
permissions: [
|
||||
{ type: 'basic', name: 'kubernetes.proxy', attributes: {} },
|
||||
{ type: 'basic', name: 'kubernetes.resource', attributes: {} },
|
||||
],
|
||||
rules: [],
|
||||
});
|
||||
|
||||
@@ -73,6 +73,7 @@ import {
|
||||
} from './KubernetesFanOutHandler';
|
||||
import { KubernetesClientBasedFetcher } from './KubernetesFetcher';
|
||||
import { KubernetesProxy } from './KubernetesProxy';
|
||||
import { requirePermission } from '../auth/requirePermission';
|
||||
|
||||
/**
|
||||
* @deprecated Please migrate to the new backend system as this will be removed in the future.
|
||||
@@ -393,6 +394,7 @@ export class KubernetesBuilder {
|
||||
);
|
||||
// @deprecated
|
||||
router.post('/services/:serviceId', async (req, res) => {
|
||||
await requirePermission(permissionApi, httpAuth, req);
|
||||
const serviceId = req.params.serviceId;
|
||||
const requestBody: ObjectsByEntityRequest = req.body;
|
||||
try {
|
||||
@@ -413,6 +415,7 @@ export class KubernetesBuilder {
|
||||
});
|
||||
|
||||
router.get('/clusters', async (req, res) => {
|
||||
await requirePermission(permissionApi, httpAuth, req);
|
||||
const credentials = await httpAuth.credentials(req);
|
||||
const clusterDetails = await this.fetchClusterDetails(clusterSupplier, {
|
||||
credentials,
|
||||
@@ -447,6 +450,7 @@ export class KubernetesBuilder {
|
||||
objectsProvider,
|
||||
authService,
|
||||
httpAuth,
|
||||
permissionApi,
|
||||
);
|
||||
|
||||
return router;
|
||||
|
||||
@@ -347,6 +347,9 @@ export interface KubernetesRequestBody {
|
||||
entity: Entity;
|
||||
}
|
||||
|
||||
// @public
|
||||
export const kubernetesResourcePermission: BasicPermission;
|
||||
|
||||
// @public (undocumented)
|
||||
export interface LimitRangeFetchResponse {
|
||||
// (undocumented)
|
||||
|
||||
@@ -25,6 +25,7 @@ export * from './catalog-entity-constants';
|
||||
export * from './certificate-authority-constants';
|
||||
export {
|
||||
kubernetesProxyPermission,
|
||||
kubernetesResourcePermission,
|
||||
kubernetesPermissions,
|
||||
} from './permissions';
|
||||
export * from './error-detection';
|
||||
|
||||
@@ -24,8 +24,19 @@ export const kubernetesProxyPermission = createPermission({
|
||||
attributes: {},
|
||||
});
|
||||
|
||||
/** This permission is used to check access to the resources endpoints
|
||||
* @public
|
||||
*/
|
||||
export const kubernetesResourcePermission = createPermission({
|
||||
name: 'kubernetes.resource',
|
||||
attributes: {},
|
||||
});
|
||||
|
||||
/**
|
||||
* List of all Kubernetes permissions.
|
||||
* @public
|
||||
*/
|
||||
export const kubernetesPermissions = [kubernetesProxyPermission];
|
||||
export const kubernetesPermissions = [
|
||||
kubernetesProxyPermission,
|
||||
kubernetesResourcePermission,
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user