@kuadrant/kuadrant-backstage-plugin-backend 0.0.2-dev-0b744dd → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,7 +9,7 @@ var Router = require('express-promise-router');
9
9
  var cors = require('cors');
10
10
  var crypto = require('crypto');
11
11
  var k8sClient = require('./k8s-client.cjs.js');
12
- var alpha = require('./alpha.cjs.js');
12
+ var module$1 = require('./module.cjs.js');
13
13
  var permissions = require('./permissions.cjs.js');
14
14
 
15
15
  function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
@@ -36,6 +36,38 @@ async function getUserIdentity(req, httpAuth, userInfo) {
36
36
  groups
37
37
  };
38
38
  }
39
+ async function verifyApiKeyUpdatePermission(credentials, userEntityRef, namespace, apiProductName, k8sClient, permissions$1) {
40
+ const updateAllDecision = await permissions$1.authorize(
41
+ [{ permission: permissions.kuadrantApiKeyUpdateAllPermission }],
42
+ { credentials }
43
+ );
44
+ if (updateAllDecision[0].result === pluginPermissionCommon.AuthorizeResult.ALLOW) {
45
+ return;
46
+ }
47
+ const updateOwnDecision = await permissions$1.authorize(
48
+ [{ permission: permissions.kuadrantApiKeyUpdateOwnPermission }],
49
+ { credentials }
50
+ );
51
+ if (updateOwnDecision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
52
+ throw new errors.NotAllowedError("unauthorised");
53
+ }
54
+ let apiProduct;
55
+ try {
56
+ apiProduct = await k8sClient.getCustomResource(
57
+ "devportal.kuadrant.io",
58
+ "v1alpha1",
59
+ namespace,
60
+ "apiproducts",
61
+ apiProductName
62
+ );
63
+ } catch (error) {
64
+ throw new errors.InputError(`APIProduct '${apiProductName}' not found in namespace '${namespace}' - cannot verify ownership`);
65
+ }
66
+ const owner = apiProduct.metadata?.annotations?.["backstage.io/owner"];
67
+ if (owner !== userEntityRef) {
68
+ throw new errors.NotAllowedError("you can only update requests for your own api products");
69
+ }
70
+ }
39
71
  async function createRouter({
40
72
  httpAuth,
41
73
  userInfo,
@@ -155,7 +187,7 @@ async function createRouter({
155
187
  "apiproducts",
156
188
  apiProduct
157
189
  );
158
- const provider = alpha.getAPIProductEntityProvider();
190
+ const provider = module$1.getAPIProductEntityProvider();
159
191
  if (provider) {
160
192
  await provider.refresh();
161
193
  }
@@ -239,7 +271,7 @@ async function createRouter({
239
271
  "apiproducts",
240
272
  name
241
273
  );
242
- const provider = alpha.getAPIProductEntityProvider();
274
+ const provider = module$1.getAPIProductEntityProvider();
243
275
  if (provider) {
244
276
  await provider.refresh();
245
277
  }
@@ -257,7 +289,7 @@ async function createRouter({
257
289
  try {
258
290
  const credentials = await httpAuth.credentials(req);
259
291
  const decision = await permissions$1.authorize(
260
- [{ permission: permissions.kuadrantApiProductListPermission }],
292
+ [{ permission: permissions.kuadrantHttpRouteListPermission }],
261
293
  { credentials }
262
294
  );
263
295
  if (decision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
@@ -274,8 +306,37 @@ async function createRouter({
274
306
  }
275
307
  }
276
308
  });
309
+ router.get("/httproutes/:namespace/:name", async (req, res) => {
310
+ try {
311
+ const credentials = await httpAuth.credentials(req);
312
+ const decision = await permissions$1.authorize(
313
+ [{ permission: permissions.kuadrantHttpRouteListPermission }],
314
+ { credentials }
315
+ );
316
+ if (decision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
317
+ throw new errors.NotAllowedError("unauthorised");
318
+ }
319
+ const { namespace, name } = req.params;
320
+ const data = await k8sClient$1.getCustomResource("gateway.networking.k8s.io", "v1", namespace, "httproutes", name);
321
+ res.json(data);
322
+ } catch (error) {
323
+ console.error("error fetching httproute:", error);
324
+ if (error instanceof errors.NotAllowedError) {
325
+ res.status(403).json({ error: error.message });
326
+ } else {
327
+ res.status(500).json({ error: "failed to fetch httproute" });
328
+ }
329
+ }
330
+ });
277
331
  router.patch("/apiproducts/:namespace/:name", async (req, res) => {
278
332
  const patchSchema = zod.z.object({
333
+ metadata: zod.z.object({
334
+ labels: zod.z.object({
335
+ // allow updating lifecycle phase via edit apiproduct dialog
336
+ // synced to backstage catalog entity's lifecycle field
337
+ lifecycle: zod.z.enum(["experimental", "production", "deprecated", "retired"]).optional()
338
+ }).partial().optional()
339
+ }).partial().optional(),
279
340
  spec: zod.z.object({
280
341
  displayName: zod.z.string().optional(),
281
342
  description: zod.z.string().optional(),
@@ -290,7 +351,7 @@ async function createRouter({
290
351
  }).partial().optional(),
291
352
  documentation: zod.z.object({
292
353
  docsURL: zod.z.string().optional(),
293
- openAPISpec: zod.z.string().optional()
354
+ openAPISpecURL: zod.z.string().optional()
294
355
  }).partial().optional()
295
356
  }).partial()
296
357
  });
@@ -326,6 +387,18 @@ async function createRouter({
326
387
  if (req.body.metadata?.annotations) {
327
388
  delete req.body.metadata.annotations["backstage.io/owner"];
328
389
  }
390
+ if (parsed.data.spec?.publishStatus === "Published" && parsed.data.metadata?.labels?.lifecycle === "retired") {
391
+ throw new errors.InputError("cannot publish a retired API product");
392
+ }
393
+ if (parsed.data.metadata?.labels?.lifecycle === "retired") {
394
+ const existing = await k8sClient$1.getCustomResource("devportal.kuadrant.io", "v1alpha1", namespace, "apiproducts", name);
395
+ if (existing.spec?.publishStatus === "Published") {
396
+ if (!parsed.data.spec) {
397
+ parsed.data.spec = {};
398
+ }
399
+ parsed.data.spec.publishStatus = "Draft";
400
+ }
401
+ }
329
402
  const updated = await k8sClient$1.patchCustomResource(
330
403
  "devportal.kuadrant.io",
331
404
  "v1alpha1",
@@ -334,7 +407,7 @@ async function createRouter({
334
407
  name,
335
408
  parsed.data
336
409
  );
337
- const provider = alpha.getAPIProductEntityProvider();
410
+ const provider = module$1.getAPIProductEntityProvider();
338
411
  if (provider) {
339
412
  await provider.refresh();
340
413
  }
@@ -368,18 +441,20 @@ async function createRouter({
368
441
  name: policy.metadata.name,
369
442
  namespace: policy.metadata.namespace
370
443
  },
371
- // only expose targetRef to allow UI to match PlanPolicy -> HTTPRoute
372
- targetRef: policy.spec?.targetRef ? {
373
- kind: policy.spec.targetRef.kind,
374
- name: policy.spec.targetRef.name,
375
- namespace: policy.spec.targetRef.namespace
376
- } : void 0,
377
- // only expose plan tier info, no other spec details
378
- plans: (policy.spec?.plans || []).map((plan) => ({
379
- tier: plan.tier,
380
- description: plan.description,
381
- limits: plan.limits
382
- }))
444
+ spec: policy.spec ? {
445
+ // expose targetRef to allow UI to match PlanPolicy -> HTTPRoute
446
+ targetRef: policy.spec.targetRef ? {
447
+ kind: policy.spec.targetRef.kind,
448
+ name: policy.spec.targetRef.name,
449
+ namespace: policy.spec.targetRef.namespace
450
+ } : void 0,
451
+ plans: (policy.spec.plans || []).map((plan) => ({
452
+ tier: plan.tier,
453
+ description: plan.description,
454
+ limits: plan.limits
455
+ }))
456
+ } : {},
457
+ status: policy.status
383
458
  }))
384
459
  };
385
460
  res.json(filtered);
@@ -479,7 +554,8 @@ async function createRouter({
479
554
  if (error instanceof errors.NotAllowedError) {
480
555
  res.status(403).json({ error: error.message });
481
556
  } else {
482
- res.status(500).json({ error: "failed to create api key request" });
557
+ const errorMessage = error instanceof Error ? error.message : "failed to create api key request";
558
+ res.status(500).json({ error: errorMessage });
483
559
  }
484
560
  }
485
561
  });
@@ -592,30 +668,14 @@ async function createRouter({
592
668
  if (!apiProductName) {
593
669
  throw new errors.InputError("apiProductRef.name is required in APIKey spec");
594
670
  }
595
- const apiProduct = await k8sClient$1.getCustomResource(
596
- "devportal.kuadrant.io",
597
- "v1alpha1",
671
+ await verifyApiKeyUpdatePermission(
672
+ credentials,
673
+ userEntityRef,
598
674
  namespace,
599
- "apiproducts",
600
- apiProductName
601
- );
602
- const owner = apiProduct.metadata?.annotations?.["backstage.io/owner"];
603
- const updateAllDecision = await permissions$1.authorize(
604
- [{ permission: permissions.kuadrantApiKeyUpdateAllPermission }],
605
- { credentials }
675
+ apiProductName,
676
+ k8sClient$1,
677
+ permissions$1
606
678
  );
607
- if (updateAllDecision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
608
- const updateOwnDecision = await permissions$1.authorize(
609
- [{ permission: permissions.kuadrantApiKeyUpdateOwnPermission }],
610
- { credentials }
611
- );
612
- if (updateOwnDecision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
613
- throw new errors.NotAllowedError("unauthorised");
614
- }
615
- if (owner !== userEntityRef) {
616
- throw new errors.NotAllowedError("you can only approve requests for your own api products");
617
- }
618
- }
619
679
  const status = {
620
680
  phase: "Approved",
621
681
  reviewedBy,
@@ -661,30 +721,14 @@ async function createRouter({
661
721
  if (!apiProductName) {
662
722
  throw new errors.InputError("apiProductRef.name is required in APIKey spec");
663
723
  }
664
- const apiProduct = await k8sClient$1.getCustomResource(
665
- "devportal.kuadrant.io",
666
- "v1alpha1",
724
+ await verifyApiKeyUpdatePermission(
725
+ credentials,
726
+ userEntityRef,
667
727
  namespace,
668
- "apiproducts",
669
- apiProductName
728
+ apiProductName,
729
+ k8sClient$1,
730
+ permissions$1
670
731
  );
671
- const owner = apiProduct.metadata?.annotations?.["backstage.io/owner"];
672
- const updateAllDecision = await permissions$1.authorize(
673
- [{ permission: permissions.kuadrantApiKeyUpdateAllPermission }],
674
- { credentials }
675
- );
676
- if (updateAllDecision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
677
- const updateOwnDecision = await permissions$1.authorize(
678
- [{ permission: permissions.kuadrantApiKeyUpdateOwnPermission }],
679
- { credentials }
680
- );
681
- if (updateOwnDecision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
682
- throw new errors.NotAllowedError("unauthorised");
683
- }
684
- if (owner !== userEntityRef) {
685
- throw new errors.NotAllowedError("you can only reject requests for your own api products");
686
- }
687
- }
688
732
  const status = {
689
733
  phase: "Rejected",
690
734
  reviewedBy,
@@ -1141,6 +1185,82 @@ async function createRouter({
1141
1185
  }
1142
1186
  }
1143
1187
  });
1188
+ router.get("/authpolicies", async (req, res) => {
1189
+ try {
1190
+ const credentials = await httpAuth.credentials(req);
1191
+ const decision = await permissions$1.authorize(
1192
+ [{ permission: permissions.kuadrantAuthPolicyListPermission }],
1193
+ { credentials }
1194
+ );
1195
+ if (decision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
1196
+ throw new errors.NotAllowedError("unauthorised");
1197
+ }
1198
+ const data = await k8sClient$1.listCustomResources("kuadrant.io", "v1", "authpolicies");
1199
+ const filtered = {
1200
+ items: (data.items || []).map((policy) => ({
1201
+ metadata: {
1202
+ name: policy.metadata.name,
1203
+ namespace: policy.metadata.namespace
1204
+ },
1205
+ spec: policy.spec ? {
1206
+ // expose targetRef to allow UI to match AuthPolicy -> HTTPRoute
1207
+ targetRef: policy.spec.targetRef ? {
1208
+ kind: policy.spec.targetRef.kind,
1209
+ name: policy.spec.targetRef.name,
1210
+ namespace: policy.spec.targetRef.namespace
1211
+ } : void 0
1212
+ } : {},
1213
+ status: policy.status
1214
+ }))
1215
+ };
1216
+ res.json(filtered);
1217
+ } catch (error) {
1218
+ console.error("error fetching authpolicies:", error);
1219
+ if (error instanceof errors.NotAllowedError) {
1220
+ res.status(403).json({ error: error.message });
1221
+ } else {
1222
+ res.status(500).json({ error: "failed to fetch authpolicies" });
1223
+ }
1224
+ }
1225
+ });
1226
+ router.get("/ratelimitpolicies", async (req, res) => {
1227
+ try {
1228
+ const credentials = await httpAuth.credentials(req);
1229
+ const decision = await permissions$1.authorize(
1230
+ [{ permission: permissions.kuadrantRateLimitPolicyListPermission }],
1231
+ { credentials }
1232
+ );
1233
+ if (decision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
1234
+ throw new errors.NotAllowedError("unauthorised");
1235
+ }
1236
+ const data = await k8sClient$1.listCustomResources("kuadrant.io", "v1", "ratelimitpolicies");
1237
+ const filtered = {
1238
+ items: (data.items || []).map((policy) => ({
1239
+ metadata: {
1240
+ name: policy.metadata.name,
1241
+ namespace: policy.metadata.namespace
1242
+ },
1243
+ spec: policy.spec ? {
1244
+ // expose targetRef to allow UI to match AuthPolicy -> HTTPRoute
1245
+ targetRef: policy.spec.targetRef ? {
1246
+ kind: policy.spec.targetRef.kind,
1247
+ name: policy.spec.targetRef.name,
1248
+ namespace: policy.spec.targetRef.namespace
1249
+ } : void 0
1250
+ } : {},
1251
+ status: policy.status
1252
+ }))
1253
+ };
1254
+ res.json(filtered);
1255
+ } catch (error) {
1256
+ console.error("error fetching ratelimitpolicies:", error);
1257
+ if (error instanceof errors.NotAllowedError) {
1258
+ res.status(403).json({ error: error.message });
1259
+ } else {
1260
+ res.status(500).json({ error: "failed to fetch ratelimitpolicies" });
1261
+ }
1262
+ }
1263
+ });
1144
1264
  router.use(pluginPermissionNode.createPermissionIntegrationRouter({
1145
1265
  permissions: permissions.kuadrantPermissions
1146
1266
  }));