Skip to content

Commit c28d564

Browse files
authored
feat(ocm)!: add basic permissions to ocm backend plugin (#1528)
* feat(ocm)!: add basic permissions to ocm backend plugin * feat(ocm): update frontend docs for backend changes * feat(ocm): add additional permission for ocm catalog entity components * feat(ocm): add support for permissions for OCM frontend * feat(ocm): fix failing ocm test by adding require permission mock
1 parent 51a35f0 commit c28d564

File tree

16 files changed

+344
-114
lines changed

16 files changed

+344
-114
lines changed

plugins/ocm-backend/dev/index.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,24 @@ export async function startStandaloneServer(
109109
},
110110
},
111111
});
112+
112113
const createEnv = makeCreateEnv(config);
113114
const catalogEnv = useHotMemoize(module, () => createEnv('catalog'));
115+
const discovery = HostDiscovery.fromConfig(config);
116+
const tokenManager = ServerTokenManager.fromConfig(config, {
117+
logger,
118+
});
119+
const permissions = ServerPermissionClient.fromConfig(config, {
120+
discovery,
121+
tokenManager,
122+
});
114123

115-
const ocmRouterOptions: RouterOptions = { logger, config };
124+
const ocmRouterOptions: RouterOptions = {
125+
logger,
126+
config,
127+
permissions,
128+
discovery,
129+
};
116130
const service = createServiceBuilder(module)
117131
.setPort(options.port)
118132
.enableCors({

plugins/ocm-backend/package.json

Lines changed: 4 additions & 3 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",
@@ -62,8 +65,6 @@
6265
"@janus-idp/cli": "1.8.5",
6366
"@backstage/plugin-auth-node": "0.4.11",
6467
"@backstage/plugin-catalog-backend": "1.21.0",
65-
"@backstage/plugin-permission-common": "0.7.13",
66-
"@backstage/plugin-permission-node": "0.7.27",
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: 104 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,37 @@
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 {
32+
AuthorizeResult,
33+
BasicPermission,
34+
} from '@backstage/plugin-permission-common';
35+
import { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node';
2336

2437
import express from 'express';
2538
import Router from 'express-promise-router';
39+
import { Request } from 'express-serve-static-core';
2640
import { Logger } from 'winston';
2741

2842
import {
2943
Cluster,
3044
ClusterOverview,
45+
ocmClusterReadPermission,
46+
ocmEntityPermissions,
47+
ocmEntityReadPermission,
3148
} from '@janus-idp/backstage-plugin-ocm-common';
3249

3350
import { readOcmConfigs } from '../helpers/config';
@@ -52,11 +69,25 @@ import { ManagedClusterInfo } from '../types';
5269
export interface RouterOptions {
5370
logger: Logger;
5471
config: Config;
72+
discovery: PluginEndpointDiscovery;
73+
permissions: PermissionsService;
74+
httpAuth?: HttpAuthService;
5575
}
5676

57-
const buildRouter = (config: Config, logger: Logger) => {
77+
const buildRouter = (
78+
config: Config,
79+
logger: Logger,
80+
httpAuth: HttpAuthService,
81+
permissions: PermissionsService,
82+
) => {
5883
const router = Router();
84+
85+
const permissionsIntegrationRouter = createPermissionIntegrationRouter({
86+
permissions: ocmEntityPermissions,
87+
});
88+
5989
router.use(express.json());
90+
router.use(permissionsIntegrationRouter);
6091

6192
const clients = Object.fromEntries(
6293
readOcmConfigs(config).map(provider => [
@@ -68,43 +99,63 @@ const buildRouter = (config: Config, logger: Logger) => {
6899
]),
69100
);
70101

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-
);
102+
const authorize = async (request: Request, permission: BasicPermission) => {
103+
const decision = (
104+
await permissions.authorize([{ permission: permission }], {
105+
credentials: await httpAuth.credentials(request),
106+
})
107+
)[0];
108+
109+
return decision;
110+
};
111+
112+
router.get('/status/:providerId/:clusterName', async (request, response) => {
113+
const decision = await authorize(request, ocmEntityReadPermission);
114+
115+
if (decision.result === AuthorizeResult.DENY) {
116+
throw new NotAllowedError('Unauthorized');
117+
}
118+
119+
const { clusterName, providerId } = request.params;
120+
logger.debug(
121+
`Incoming status request for ${clusterName} cluster on ${providerId} hub`,
122+
);
123+
124+
if (!clients.hasOwnProperty(providerId)) {
125+
throw Object.assign(new Error('Hub not found'), {
126+
statusCode: 404,
127+
name: 'HubNotFound',
128+
});
129+
}
130+
131+
const normalizedClusterName = translateResourceToOCM(
132+
clusterName,
133+
clients[providerId].hubResourceName,
134+
);
135+
136+
const mc = await getManagedCluster(
137+
clients[providerId].client,
138+
normalizedClusterName,
139+
);
140+
const mci = await getManagedClusterInfo(
141+
clients[providerId].client,
142+
normalizedClusterName,
143+
);
144+
145+
response.send({
146+
name: clusterName,
147+
...parseManagedCluster(mc),
148+
...parseUpdateInfo(mci),
149+
} as Cluster);
150+
});
151+
152+
router.get('/status', async (request, response) => {
153+
const decision = await authorize(request, ocmClusterReadPermission);
154+
155+
if (decision.result === AuthorizeResult.DENY) {
156+
throw new NotAllowedError('Unauthorized');
157+
}
106158

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

110161
const allClusters = await Promise.all(
@@ -144,8 +195,11 @@ export async function createRouter(
144195
): Promise<express.Router> {
145196
const { logger } = options;
146197
const { config } = options;
198+
const { permissions } = options;
199+
200+
const { httpAuth } = createLegacyAuthAdapters(options);
147201

148-
return buildRouter(config, logger);
202+
return buildRouter(config, logger, httpAuth, permissions);
149203
}
150204

151205
export const ocmPlugin = createBackendPlugin({
@@ -156,9 +210,18 @@ export const ocmPlugin = createBackendPlugin({
156210
logger: coreServices.logger,
157211
config: coreServices.rootConfig,
158212
http: coreServices.httpRouter,
213+
httpAuth: coreServices.httpAuth,
214+
permissions: coreServices.permissions,
159215
},
160-
async init({ config, logger, http }) {
161-
http.use(buildRouter(config, loggerToWinstonLogger(logger)));
216+
async init({ config, logger, http, httpAuth, permissions }) {
217+
http.use(
218+
buildRouter(
219+
config,
220+
loggerToWinstonLogger(logger),
221+
httpAuth,
222+
permissions,
223+
),
224+
);
162225
},
163226
});
164227
},

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
}

0 commit comments

Comments
 (0)