Skip to content

Commit 85fede3

Browse files
committed
feat(ocm): add basic permissions to ocm backend plugin
1 parent 397fb8e commit 85fede3

File tree

7 files changed

+227
-98
lines changed

7 files changed

+227
-98
lines changed

plugins/ocm-backend/package.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,18 @@
4242
"configSchema": "config.d.ts",
4343
"dependencies": {
4444
"@backstage/backend-common": "^0.21.6",
45+
"@backstage/backend-dynamic-feature-service": "^0.2.8",
4546
"@backstage/backend-plugin-api": "^0.6.16",
4647
"@backstage/backend-tasks": "^0.5.21",
4748
"@backstage/catalog-client": "^1.6.3",
4849
"@backstage/catalog-model": "^1.4.5",
4950
"@backstage/config": "^1.2.0",
51+
"@backstage/errors": "^1.2.4",
5052
"@backstage/plugin-catalog-node": "^1.11.0",
5153
"@backstage/plugin-kubernetes-common": "^0.7.5",
54+
"@backstage/plugin-permission-common": "0.7.13",
55+
"@backstage/plugin-permission-node": "0.7.27",
5256
"@janus-idp/backstage-plugin-ocm-common": "2.3.0",
53-
"@backstage/backend-dynamic-feature-service": "^0.2.8",
5457
"@kubernetes/client-node": "^0.20.0",
5558
"express": "^4.18.2",
5659
"express-promise-router": "^4.1.1",
@@ -59,11 +62,9 @@
5962
},
6063
"devDependencies": {
6164
"@backstage/cli": "0.26.2",
62-
"@janus-idp/cli": "1.8.0",
6365
"@backstage/plugin-auth-node": "0.4.11",
6466
"@backstage/plugin-catalog-backend": "1.21.0",
65-
"@backstage/plugin-permission-common": "0.7.13",
66-
"@backstage/plugin-permission-node": "0.7.27",
67+
"@janus-idp/cli": "1.8.0",
6768
"@types/express": "4.17.20",
6869
"@types/supertest": "2.0.16",
6970
"msw": "1.3.2",

plugins/ocm-backend/src/service/router.test.ts

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ConfigReader } from '@backstage/config';
2+
import { AuthorizeResult } from '@backstage/plugin-permission-common';
23

34
import express from 'express';
45
import { setupServer } from 'msw/node';
@@ -26,11 +27,33 @@ const logger = createLogger({
2627
transports: [new transports.Console({ silent: true })],
2728
});
2829

30+
const mockedAuthorize = jest.fn().mockImplementation(async () => [
31+
{
32+
result: AuthorizeResult.ALLOW,
33+
},
34+
]);
35+
36+
const mockedAuthorizeConditional = jest.fn().mockImplementation(async () => [
37+
{
38+
result: AuthorizeResult.ALLOW,
39+
},
40+
]);
41+
42+
const mockPermissionEvaluator = {
43+
authorize: mockedAuthorize,
44+
authorizeConditional: mockedAuthorizeConditional,
45+
};
46+
47+
const mockDiscovery = {
48+
getBaseUrl: jest.fn(),
49+
getExternalBaseUrl: jest.fn(),
50+
};
51+
2952
describe('createRouter', () => {
3053
let app: express.Express;
3154

3255
beforeAll(async () => {
33-
jest.resetAllMocks();
56+
jest.clearAllMocks();
3457
const router = await createRouter({
3558
logger: logger,
3659
config: new ConfigReader({
@@ -46,11 +69,28 @@ describe('createRouter', () => {
4669
},
4770
},
4871
}),
72+
permissions: mockPermissionEvaluator,
73+
discovery: mockDiscovery,
4974
});
5075
app = express().use(router);
5176
});
5277

5378
describe('GET /status', () => {
79+
it('should deny access when getting all clusters', async () => {
80+
mockedAuthorize.mockImplementationOnce(async () => [
81+
{ result: AuthorizeResult.DENY },
82+
]);
83+
84+
const result = await request(app).get('/status');
85+
86+
expect(mockedAuthorize).toHaveBeenCalled();
87+
88+
expect(result.statusCode).toBe(403);
89+
expect(result.body.error).toEqual({
90+
name: 'NotAllowedError',
91+
message: 'Unauthorized',
92+
});
93+
});
5494
it('should get all clusters', async () => {
5595
const result = await request(app).get('/status');
5696

@@ -177,6 +217,21 @@ describe('createRouter', () => {
177217
});
178218

179219
describe('GET /status/:hubName/:clusterName', () => {
220+
it('should deny access when getting all clusters', async () => {
221+
mockedAuthorize.mockImplementationOnce(async () => [
222+
{ result: AuthorizeResult.DENY },
223+
]);
224+
225+
const result = await request(app).get('/status/foo/cluster1');
226+
227+
expect(mockedAuthorize).toHaveBeenCalled();
228+
229+
expect(result.statusCode).toBe(403);
230+
expect(result.body.error).toEqual({
231+
name: 'NotAllowedError',
232+
message: 'Unauthorized',
233+
});
234+
});
180235
it('should correctly parse a cluster', async () => {
181236
const result = await request(app).get('/status/foo/cluster1');
182237

plugins/ocm-backend/src/service/router.ts

Lines changed: 100 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,33 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { errorHandler, loggerToWinstonLogger } from '@backstage/backend-common';
17+
import {
18+
createLegacyAuthAdapters,
19+
errorHandler,
20+
loggerToWinstonLogger,
21+
PluginEndpointDiscovery,
22+
} from '@backstage/backend-common';
1823
import {
1924
coreServices,
2025
createBackendPlugin,
26+
HttpAuthService,
27+
PermissionsService,
2128
} from '@backstage/backend-plugin-api';
2229
import { Config } from '@backstage/config';
30+
import { NotAllowedError } from '@backstage/errors';
31+
import { AuthorizeResult } from '@backstage/plugin-permission-common';
32+
import { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node';
2333

2434
import express from 'express';
2535
import Router from 'express-promise-router';
36+
import { Request } from 'express-serve-static-core';
2637
import { Logger } from 'winston';
2738

2839
import {
2940
Cluster,
3041
ClusterOverview,
42+
ocmClusterReadPermission,
43+
ocmEntityPermissions,
3144
} from '@janus-idp/backstage-plugin-ocm-common';
3245

3346
import { readOcmConfigs } from '../helpers/config';
@@ -52,11 +65,25 @@ import { ManagedClusterInfo } from '../types';
5265
export interface RouterOptions {
5366
logger: Logger;
5467
config: Config;
68+
discovery: PluginEndpointDiscovery;
69+
permissions: PermissionsService;
70+
httpAuth?: HttpAuthService;
5571
}
5672

57-
const buildRouter = (config: Config, logger: Logger) => {
73+
const buildRouter = (
74+
config: Config,
75+
logger: Logger,
76+
httpAuth: HttpAuthService,
77+
permissions: PermissionsService,
78+
) => {
5879
const router = Router();
80+
81+
const permissionsIntegrationRouter = createPermissionIntegrationRouter({
82+
permissions: ocmEntityPermissions,
83+
});
84+
5985
router.use(express.json());
86+
router.use(permissionsIntegrationRouter);
6087

6188
const clients = Object.fromEntries(
6289
readOcmConfigs(config).map(provider => [
@@ -68,43 +95,63 @@ const buildRouter = (config: Config, logger: Logger) => {
6895
]),
6996
);
7097

71-
router.get(
72-
'/status/:providerId/:clusterName',
73-
async ({ params: { clusterName, providerId } }, response) => {
74-
logger.debug(
75-
`Incoming status request for ${clusterName} cluster on ${providerId} hub`,
76-
);
77-
78-
if (!clients.hasOwnProperty(providerId)) {
79-
throw Object.assign(new Error('Hub not found'), {
80-
statusCode: 404,
81-
name: 'HubNotFound',
82-
});
83-
}
84-
85-
const normalizedClusterName = translateResourceToOCM(
86-
clusterName,
87-
clients[providerId].hubResourceName,
88-
);
89-
90-
const mc = await getManagedCluster(
91-
clients[providerId].client,
92-
normalizedClusterName,
93-
);
94-
const mci = await getManagedClusterInfo(
95-
clients[providerId].client,
96-
normalizedClusterName,
97-
);
98-
99-
response.send({
100-
name: clusterName,
101-
...parseManagedCluster(mc),
102-
...parseUpdateInfo(mci),
103-
} as Cluster);
104-
},
105-
);
98+
const authorize = async (request: Request) => {
99+
const decision = (
100+
await permissions.authorize([{ permission: ocmClusterReadPermission }], {
101+
credentials: await httpAuth.credentials(request),
102+
})
103+
)[0];
104+
105+
return decision;
106+
};
107+
108+
router.get('/status/:providerId/:clusterName', async (request, response) => {
109+
const decision = await authorize(request);
110+
111+
if (decision.result === AuthorizeResult.DENY) {
112+
throw new NotAllowedError('Unauthorized');
113+
}
114+
115+
const { clusterName, providerId } = request.params;
116+
logger.debug(
117+
`Incoming status request for ${clusterName} cluster on ${providerId} hub`,
118+
);
119+
120+
if (!clients.hasOwnProperty(providerId)) {
121+
throw Object.assign(new Error('Hub not found'), {
122+
statusCode: 404,
123+
name: 'HubNotFound',
124+
});
125+
}
126+
127+
const normalizedClusterName = translateResourceToOCM(
128+
clusterName,
129+
clients[providerId].hubResourceName,
130+
);
131+
132+
const mc = await getManagedCluster(
133+
clients[providerId].client,
134+
normalizedClusterName,
135+
);
136+
const mci = await getManagedClusterInfo(
137+
clients[providerId].client,
138+
normalizedClusterName,
139+
);
140+
141+
response.send({
142+
name: clusterName,
143+
...parseManagedCluster(mc),
144+
...parseUpdateInfo(mci),
145+
} as Cluster);
146+
});
147+
148+
router.get('/status', async (request, response) => {
149+
const decision = await authorize(request);
150+
151+
if (decision.result === AuthorizeResult.DENY) {
152+
throw new NotAllowedError('Unauthorized');
153+
}
106154

107-
router.get('/status', async (_, response) => {
108155
logger.debug(`Incoming status request for all clusters`);
109156

110157
const allClusters = await Promise.all(
@@ -144,8 +191,11 @@ export async function createRouter(
144191
): Promise<express.Router> {
145192
const { logger } = options;
146193
const { config } = options;
194+
const { permissions } = options;
195+
196+
const { httpAuth } = createLegacyAuthAdapters(options);
147197

148-
return buildRouter(config, logger);
198+
return buildRouter(config, logger, httpAuth, permissions);
149199
}
150200

151201
export const ocmPlugin = createBackendPlugin({
@@ -156,9 +206,18 @@ export const ocmPlugin = createBackendPlugin({
156206
logger: coreServices.logger,
157207
config: coreServices.rootConfig,
158208
http: coreServices.httpRouter,
209+
httpAuth: coreServices.httpAuth,
210+
permissions: coreServices.permissions,
159211
},
160-
async init({ config, logger, http }) {
161-
http.use(buildRouter(config, loggerToWinstonLogger(logger)));
212+
async init({ config, logger, http, httpAuth, permissions }) {
213+
http.use(
214+
buildRouter(
215+
config,
216+
loggerToWinstonLogger(logger),
217+
httpAuth,
218+
permissions,
219+
),
220+
);
162221
},
163222
});
164223
},

plugins/ocm-common/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,5 +35,8 @@
3535
"plugin"
3636
],
3737
"homepage": "https://janus-idp.io/",
38-
"bugs": "https://github.com/janus-idp/backstage-plugins/issues"
38+
"bugs": "https://github.com/janus-idp/backstage-plugins/issues",
39+
"dependencies": {
40+
"@backstage/plugin-permission-common": "^0.7.13"
41+
}
3942
}

plugins/ocm-common/src/index.ts

Lines changed: 2 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,11 @@
1-
/***/
21
/**
32
* Common functionalities for the Open Cluster Management plugin.
43
*
54
* @packageDocumentation
65
*/
76

8-
export type ClusterStatus = {
9-
available: boolean;
10-
reason?: string;
11-
};
12-
13-
export type ClusterBase = {
14-
name: string;
15-
};
16-
17-
export type ClusterUpdate = {
18-
available?: boolean;
19-
version?: string;
20-
url?: string;
21-
};
22-
23-
export type ClusterNodesStatus = {
24-
status: string;
25-
type: string;
26-
};
27-
28-
export type ClusterDetails = {
29-
consoleUrl?: string;
30-
kubernetesVersion?: string;
31-
oauthUrl?: string;
32-
openshiftId?: string;
33-
openshiftVersion?: string;
34-
platform?: string;
35-
region?: string;
36-
allocatableResources?: {
37-
cpuCores?: number;
38-
memorySize?: string;
39-
numberOfPods?: number;
40-
};
41-
availableResources?: {
42-
cpuCores?: number;
43-
memorySize?: string;
44-
numberOfPods?: number;
45-
};
46-
update?: ClusterUpdate;
47-
status: ClusterStatus;
48-
};
49-
50-
export type Cluster = ClusterBase & ClusterDetails;
51-
export type ClusterOverview = ClusterBase & {
52-
status: ClusterStatus;
53-
update: ClusterUpdate;
54-
platform: string;
55-
openshiftVersion: string;
56-
nodes: Array<ClusterNodesStatus>;
57-
};
7+
export * from './types';
8+
export * from './permissions';
589

5910
export const ANNOTATION_CLUSTER_ID = 'janus-idp.io/ocm-cluster-id';
6011
export const ANNOTATION_PROVIDER_ID = 'janus-idp.io/ocm-provider-id';

0 commit comments

Comments
 (0)