@kuadrant/kuadrant-backstage-plugin-backend 0.0.2-dev-cd184fc → 0.0.2-dev-18c7ab0

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 }; }
@@ -23,6 +23,46 @@ function extractNameFromEntityRef(entityRef) {
23
23
  const parts = entityRef.split("/");
24
24
  return parts[parts.length - 1];
25
25
  }
26
+ function getConsumerNamespace(userEntityRef) {
27
+ const username = extractNameFromEntityRef(userEntityRef);
28
+ if (!username || username.trim() === "") {
29
+ throw new Error("Invalid user identity - username is empty");
30
+ }
31
+ const sanitized = username.toLowerCase().replace(/[^a-z0-9-]/g, "-");
32
+ if (sanitized === "" || sanitized.startsWith("-") || sanitized.endsWith("-")) {
33
+ throw new Error(`Username "${username}" cannot be converted to valid namespace name`);
34
+ }
35
+ const hash = crypto.createHash("sha256").update(userEntityRef).digest("hex").substring(0, 8);
36
+ return `kuadrant-${sanitized}-${hash}`;
37
+ }
38
+ async function ensureConsumerNamespace(k8sClient, namespace) {
39
+ try {
40
+ await k8sClient.getNamespace(namespace);
41
+ } catch (error) {
42
+ const statusCode = error.statusCode || error.response?.statusCode || error.body?.code;
43
+ if (statusCode === 404) {
44
+ try {
45
+ await k8sClient.createNamespace({
46
+ apiVersion: "v1",
47
+ kind: "Namespace",
48
+ metadata: {
49
+ name: namespace,
50
+ labels: {
51
+ "devportal.kuadrant.io/consumer-namespace": "true"
52
+ }
53
+ }
54
+ });
55
+ } catch (createError) {
56
+ const createStatusCode = createError.statusCode || createError.response?.statusCode || createError.body?.code;
57
+ if (createStatusCode !== 409) {
58
+ throw createError;
59
+ }
60
+ }
61
+ } else {
62
+ throw error;
63
+ }
64
+ }
65
+ }
26
66
  async function getUserIdentity(req, httpAuth, userInfo) {
27
67
  const credentials = await httpAuth.credentials(req);
28
68
  if (!credentials || !credentials.principal) {
@@ -36,6 +76,38 @@ async function getUserIdentity(req, httpAuth, userInfo) {
36
76
  groups
37
77
  };
38
78
  }
79
+ async function verifyApiKeyUpdatePermission(credentials, userEntityRef, namespace, apiProductName, k8sClient, permissions$1) {
80
+ const updateAllDecision = await permissions$1.authorize(
81
+ [{ permission: permissions.kuadrantApiKeyUpdateAllPermission }],
82
+ { credentials }
83
+ );
84
+ if (updateAllDecision[0].result === pluginPermissionCommon.AuthorizeResult.ALLOW) {
85
+ return;
86
+ }
87
+ const updateOwnDecision = await permissions$1.authorize(
88
+ [{ permission: permissions.kuadrantApiKeyUpdateOwnPermission }],
89
+ { credentials }
90
+ );
91
+ if (updateOwnDecision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
92
+ throw new errors.NotAllowedError("unauthorised");
93
+ }
94
+ let apiProduct;
95
+ try {
96
+ apiProduct = await k8sClient.getCustomResource(
97
+ "devportal.kuadrant.io",
98
+ "v1alpha1",
99
+ namespace,
100
+ "apiproducts",
101
+ apiProductName
102
+ );
103
+ } catch (error) {
104
+ throw new errors.InputError(`APIProduct '${apiProductName}' not found in namespace '${namespace}' - cannot verify ownership`);
105
+ }
106
+ const owner = apiProduct.metadata?.annotations?.["backstage.io/owner"];
107
+ if (owner !== userEntityRef) {
108
+ throw new errors.NotAllowedError("you can only update requests for your own api products");
109
+ }
110
+ }
39
111
  async function createRouter({
40
112
  httpAuth,
41
113
  userInfo,
@@ -155,7 +227,7 @@ async function createRouter({
155
227
  "apiproducts",
156
228
  apiProduct
157
229
  );
158
- const provider = alpha.getAPIProductEntityProvider();
230
+ const provider = module$1.getAPIProductEntityProvider();
159
231
  if (provider) {
160
232
  await provider.refresh();
161
233
  }
@@ -201,8 +273,7 @@ async function createRouter({
201
273
  allRequests = await k8sClient$1.listCustomResources(
202
274
  "devportal.kuadrant.io",
203
275
  "v1alpha1",
204
- "apikeys",
205
- namespace
276
+ "apikeys"
206
277
  );
207
278
  } catch (error) {
208
279
  console.warn("failed to list apikeys during cascade delete:", error);
@@ -215,11 +286,12 @@ async function createRouter({
215
286
  const deletionResults = await Promise.allSettled(
216
287
  relatedRequests.map(async (request) => {
217
288
  const requestName = request.metadata.name;
218
- console.log(`deleting apikey: ${namespace}/${requestName}`);
289
+ const requestNamespace = request.metadata.namespace;
290
+ console.log(`deleting apikey: ${requestNamespace}/${requestName}`);
219
291
  await k8sClient$1.deleteCustomResource(
220
292
  "devportal.kuadrant.io",
221
293
  "v1alpha1",
222
- namespace,
294
+ requestNamespace,
223
295
  "apikeys",
224
296
  requestName
225
297
  );
@@ -239,7 +311,7 @@ async function createRouter({
239
311
  "apiproducts",
240
312
  name
241
313
  );
242
- const provider = alpha.getAPIProductEntityProvider();
314
+ const provider = module$1.getAPIProductEntityProvider();
243
315
  if (provider) {
244
316
  await provider.refresh();
245
317
  }
@@ -257,7 +329,7 @@ async function createRouter({
257
329
  try {
258
330
  const credentials = await httpAuth.credentials(req);
259
331
  const decision = await permissions$1.authorize(
260
- [{ permission: permissions.kuadrantApiProductListPermission }],
332
+ [{ permission: permissions.kuadrantHttpRouteListPermission }],
261
333
  { credentials }
262
334
  );
263
335
  if (decision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
@@ -274,8 +346,37 @@ async function createRouter({
274
346
  }
275
347
  }
276
348
  });
349
+ router.get("/httproutes/:namespace/:name", async (req, res) => {
350
+ try {
351
+ const credentials = await httpAuth.credentials(req);
352
+ const decision = await permissions$1.authorize(
353
+ [{ permission: permissions.kuadrantHttpRouteListPermission }],
354
+ { credentials }
355
+ );
356
+ if (decision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
357
+ throw new errors.NotAllowedError("unauthorised");
358
+ }
359
+ const { namespace, name } = req.params;
360
+ const data = await k8sClient$1.getCustomResource("gateway.networking.k8s.io", "v1", namespace, "httproutes", name);
361
+ res.json(data);
362
+ } catch (error) {
363
+ console.error("error fetching httproute:", error);
364
+ if (error instanceof errors.NotAllowedError) {
365
+ res.status(403).json({ error: error.message });
366
+ } else {
367
+ res.status(500).json({ error: "failed to fetch httproute" });
368
+ }
369
+ }
370
+ });
277
371
  router.patch("/apiproducts/:namespace/:name", async (req, res) => {
278
372
  const patchSchema = zod.z.object({
373
+ metadata: zod.z.object({
374
+ labels: zod.z.object({
375
+ // allow updating lifecycle phase via edit apiproduct dialog
376
+ // synced to backstage catalog entity's lifecycle field
377
+ lifecycle: zod.z.enum(["experimental", "production", "deprecated", "retired"]).optional()
378
+ }).partial().optional()
379
+ }).partial().optional(),
279
380
  spec: zod.z.object({
280
381
  displayName: zod.z.string().optional(),
281
382
  description: zod.z.string().optional(),
@@ -290,7 +391,7 @@ async function createRouter({
290
391
  }).partial().optional(),
291
392
  documentation: zod.z.object({
292
393
  docsURL: zod.z.string().optional(),
293
- openAPISpec: zod.z.string().optional()
394
+ openAPISpecURL: zod.z.string().optional()
294
395
  }).partial().optional()
295
396
  }).partial()
296
397
  });
@@ -326,6 +427,18 @@ async function createRouter({
326
427
  if (req.body.metadata?.annotations) {
327
428
  delete req.body.metadata.annotations["backstage.io/owner"];
328
429
  }
430
+ if (parsed.data.spec?.publishStatus === "Published" && parsed.data.metadata?.labels?.lifecycle === "retired") {
431
+ throw new errors.InputError("cannot publish a retired API product");
432
+ }
433
+ if (parsed.data.metadata?.labels?.lifecycle === "retired") {
434
+ const existing = await k8sClient$1.getCustomResource("devportal.kuadrant.io", "v1alpha1", namespace, "apiproducts", name);
435
+ if (existing.spec?.publishStatus === "Published") {
436
+ if (!parsed.data.spec) {
437
+ parsed.data.spec = {};
438
+ }
439
+ parsed.data.spec.publishStatus = "Draft";
440
+ }
441
+ }
329
442
  const updated = await k8sClient$1.patchCustomResource(
330
443
  "devportal.kuadrant.io",
331
444
  "v1alpha1",
@@ -334,7 +447,7 @@ async function createRouter({
334
447
  name,
335
448
  parsed.data
336
449
  );
337
- const provider = alpha.getAPIProductEntityProvider();
450
+ const provider = module$1.getAPIProductEntityProvider();
338
451
  if (provider) {
339
452
  await provider.refresh();
340
453
  }
@@ -368,18 +481,20 @@ async function createRouter({
368
481
  name: policy.metadata.name,
369
482
  namespace: policy.metadata.namespace
370
483
  },
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
- }))
484
+ spec: policy.spec ? {
485
+ // expose targetRef to allow UI to match PlanPolicy -> HTTPRoute
486
+ targetRef: policy.spec.targetRef ? {
487
+ kind: policy.spec.targetRef.kind,
488
+ name: policy.spec.targetRef.name,
489
+ namespace: policy.spec.targetRef.namespace
490
+ } : void 0,
491
+ plans: (policy.spec.plans || []).map((plan) => ({
492
+ tier: plan.tier,
493
+ description: plan.description,
494
+ limits: plan.limits
495
+ }))
496
+ } : {},
497
+ status: policy.status
383
498
  }))
384
499
  };
385
500
  res.json(filtered);
@@ -414,14 +529,93 @@ async function createRouter({
414
529
  }
415
530
  }
416
531
  });
532
+ const secretSchema = zod.z.object({
533
+ name: zod.z.string().min(1),
534
+ apiKeyValue: zod.z.string().min(1)
535
+ });
536
+ router.post("/secrets", async (req, res) => {
537
+ try {
538
+ const parsed = secretSchema.safeParse(req.body);
539
+ if (!parsed.success) {
540
+ throw new errors.InputError(parsed.error.toString());
541
+ }
542
+ const credentials = await httpAuth.credentials(req);
543
+ const { userEntityRef } = await getUserIdentity(req, httpAuth, userInfo);
544
+ const consumerNamespace = getConsumerNamespace(userEntityRef);
545
+ const resourceRef = `secret:${consumerNamespace}/*`;
546
+ const decision = await permissions$1.authorize(
547
+ [{
548
+ permission: permissions.kuadrantApiKeyCreatePermission,
549
+ resourceRef
550
+ }],
551
+ { credentials }
552
+ );
553
+ if (decision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
554
+ throw new errors.NotAllowedError("not authorized to create secrets");
555
+ }
556
+ await ensureConsumerNamespace(k8sClient$1, consumerNamespace);
557
+ const { name, apiKeyValue } = parsed.data;
558
+ const secret = {
559
+ apiVersion: "v1",
560
+ kind: "Secret",
561
+ metadata: {
562
+ name,
563
+ namespace: consumerNamespace
564
+ },
565
+ type: "Opaque",
566
+ data: {
567
+ api_key: Buffer.from(apiKeyValue).toString("base64")
568
+ }
569
+ };
570
+ const created = await k8sClient$1.createSecret(consumerNamespace, secret);
571
+ res.status(201).json(created);
572
+ } catch (error) {
573
+ console.error("error creating secret:", error);
574
+ if (error instanceof errors.NotAllowedError) {
575
+ res.status(403).json({ error: error.message });
576
+ } else if (error instanceof errors.InputError) {
577
+ res.status(400).json({ error: error.message });
578
+ } else {
579
+ const errorMessage = error instanceof Error ? error.message : "failed to create secret";
580
+ res.status(500).json({ error: errorMessage });
581
+ }
582
+ }
583
+ });
584
+ router.delete("/secrets/:name", async (req, res) => {
585
+ try {
586
+ const credentials = await httpAuth.credentials(req);
587
+ const { userEntityRef } = await getUserIdentity(req, httpAuth, userInfo);
588
+ const { name } = req.params;
589
+ const consumerNamespace = getConsumerNamespace(userEntityRef);
590
+ const decision = await permissions$1.authorize(
591
+ [{ permission: permissions.kuadrantApiKeyCreatePermission, resourceRef: `secret:${consumerNamespace}/*` }],
592
+ { credentials }
593
+ );
594
+ if (decision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
595
+ throw new errors.NotAllowedError("not authorized to delete secrets");
596
+ }
597
+ await k8sClient$1.deleteSecret(consumerNamespace, name);
598
+ res.status(204).send();
599
+ } catch (error) {
600
+ console.error("error deleting secret:", error);
601
+ if (error instanceof errors.NotAllowedError) {
602
+ res.status(403).json({ error: error.message });
603
+ } else {
604
+ const errorMessage = error instanceof Error ? error.message : "failed to delete secret";
605
+ res.status(500).json({ error: errorMessage });
606
+ }
607
+ }
608
+ });
417
609
  const requestSchema = zod.z.object({
418
610
  apiProductName: zod.z.string(),
419
611
  // name of the APIProduct
420
612
  namespace: zod.z.string(),
421
- // namespace where both APIProduct and APIKey live
613
+ // owner's namespace (where the APIProduct lives)
422
614
  planTier: zod.z.string(),
423
615
  useCase: zod.z.string().optional(),
424
- userEmail: zod.z.string().optional()
616
+ userEmail: zod.z.string().optional(),
617
+ secretName: zod.z.string()
618
+ // frontend creates secret via POST /secrets first
425
619
  });
426
620
  router.post("/requests", async (req, res) => {
427
621
  const parsed = requestSchema.safeParse(req.body);
@@ -430,7 +624,7 @@ async function createRouter({
430
624
  }
431
625
  try {
432
626
  const credentials = await httpAuth.credentials(req);
433
- const { apiProductName, namespace, planTier, useCase, userEmail } = parsed.data;
627
+ const { apiProductName, namespace, planTier, useCase, userEmail, secretName: frontendSecretName } = parsed.data;
434
628
  const { userEntityRef } = await getUserIdentity(req, httpAuth, userInfo);
435
629
  const resourceRef = `apiproduct:${namespace}/${apiProductName}`;
436
630
  const decision = await permissions$1.authorize(
@@ -443,23 +637,33 @@ async function createRouter({
443
637
  if (decision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
444
638
  throw new errors.NotAllowedError(`not authorised to request access to ${apiProductName}`);
445
639
  }
640
+ const consumerNamespace = getConsumerNamespace(userEntityRef);
641
+ await ensureConsumerNamespace(k8sClient$1, consumerNamespace);
446
642
  const randomSuffix = crypto.randomBytes(4).toString("hex");
447
- const userName = extractNameFromEntityRef(userEntityRef);
448
- const requestName = `${userName}-${apiProductName}-${randomSuffix}`.toLowerCase().replace(/[^a-z0-9-]/g, "-");
643
+ const requestName = `${apiProductName}-${randomSuffix}`.toLowerCase().replace(/[^a-z0-9-]/g, "-");
449
644
  const requestedBy = { userId: userEntityRef };
450
645
  if (userEmail) {
451
646
  requestedBy.email = userEmail;
452
647
  }
648
+ if (!frontendSecretName) {
649
+ throw new errors.InputError("secretName is required - create the secret via POST /secrets first");
650
+ }
651
+ const secretName = frontendSecretName;
453
652
  const request = {
454
653
  apiVersion: "devportal.kuadrant.io/v1alpha1",
455
654
  kind: "APIKey",
456
655
  metadata: {
457
656
  name: requestName,
458
- namespace
657
+ namespace: consumerNamespace
459
658
  },
460
659
  spec: {
461
660
  apiProductRef: {
462
- name: apiProductName
661
+ name: apiProductName,
662
+ namespace
663
+ // cross-namespace reference to owner's APIProduct
664
+ },
665
+ secretRef: {
666
+ name: secretName
463
667
  },
464
668
  planTier,
465
669
  useCase: useCase || "",
@@ -469,7 +673,7 @@ async function createRouter({
469
673
  const created = await k8sClient$1.createCustomResource(
470
674
  "devportal.kuadrant.io",
471
675
  "v1alpha1",
472
- namespace,
676
+ consumerNamespace,
473
677
  "apikeys",
474
678
  request
475
679
  );
@@ -479,7 +683,8 @@ async function createRouter({
479
683
  if (error instanceof errors.NotAllowedError) {
480
684
  res.status(403).json({ error: error.message });
481
685
  } else {
482
- res.status(500).json({ error: "failed to create api key request" });
686
+ const errorMessage = error instanceof Error ? error.message : "failed to create api key request";
687
+ res.status(500).json({ error: errorMessage });
483
688
  }
484
689
  }
485
690
  });
@@ -502,11 +707,13 @@ async function createRouter({
502
707
  }
503
708
  const status = req.query.status;
504
709
  const namespace = req.query.namespace;
710
+ const apiProductName = req.query.apiProductName;
711
+ const apiProductNamespace = req.query.apiProductNamespace;
505
712
  let data;
506
713
  if (namespace) {
507
- data = await k8sClient$1.listCustomResources("devportal.kuadrant.io", "v1alpha1", "apikeys", namespace);
714
+ data = await k8sClient$1.listCustomResources("devportal.kuadrant.io", "v1alpha1", "apikeyrequests", namespace);
508
715
  } else {
509
- data = await k8sClient$1.listCustomResources("devportal.kuadrant.io", "v1alpha1", "apikeys");
716
+ data = await k8sClient$1.listCustomResources("devportal.kuadrant.io", "v1alpha1", "apikeyrequests");
510
717
  }
511
718
  let filteredItems = data.items || [];
512
719
  if (!canReadAll) {
@@ -520,9 +727,32 @@ async function createRouter({
520
727
  (req2) => ownedApiProducts.includes(req2.spec?.apiProductRef?.name)
521
728
  );
522
729
  }
730
+ if (apiProductName) {
731
+ filteredItems = filteredItems.filter(
732
+ (req2) => req2.spec?.apiProductRef?.name === apiProductName
733
+ );
734
+ }
735
+ if (apiProductNamespace) {
736
+ filteredItems = filteredItems.filter(
737
+ (req2) => req2.spec?.apiProductRef?.namespace === apiProductNamespace
738
+ );
739
+ }
523
740
  if (status) {
524
741
  filteredItems = filteredItems.filter((req2) => {
525
- const phase = req2.status?.phase || "Pending";
742
+ let phase = "Pending";
743
+ if (req2.status?.conditions && req2.status.conditions.length > 0) {
744
+ const approved = req2.status.conditions.find(
745
+ (c) => c.type === "Approved" && c.status === "True"
746
+ );
747
+ if (approved) {
748
+ phase = "Approved";
749
+ } else {
750
+ const denied = req2.status.conditions.find(
751
+ (c) => c.type === "Denied" && c.status === "True"
752
+ );
753
+ if (denied) phase = "Denied";
754
+ }
755
+ }
526
756
  return phase === status;
527
757
  });
528
758
  }
@@ -547,16 +777,32 @@ async function createRouter({
547
777
  throw new errors.NotAllowedError("unauthorised");
548
778
  }
549
779
  const { userEntityRef } = await getUserIdentity(req, httpAuth, userInfo);
550
- const namespace = req.query.namespace;
780
+ const apiProductName = req.query.apiProductName;
781
+ const apiProductNamespace = req.query.apiProductNamespace;
782
+ const consumerNs = getConsumerNamespace(userEntityRef);
551
783
  let data;
552
- if (namespace) {
553
- data = await k8sClient$1.listCustomResources("devportal.kuadrant.io", "v1alpha1", "apikeys", namespace);
554
- } else {
555
- data = await k8sClient$1.listCustomResources("devportal.kuadrant.io", "v1alpha1", "apikeys");
784
+ try {
785
+ data = await k8sClient$1.listCustomResources("devportal.kuadrant.io", "v1alpha1", "apikeys", consumerNs);
786
+ } catch (error) {
787
+ if (error.message?.includes("404") || error.statusCode === 404) {
788
+ res.json({ items: [] });
789
+ return;
790
+ }
791
+ throw error;
556
792
  }
557
- const filteredItems = (data.items || []).filter(
793
+ let filteredItems = (data.items || []).filter(
558
794
  (req2) => req2.spec?.requestedBy?.userId === userEntityRef
559
795
  );
796
+ if (apiProductName) {
797
+ filteredItems = filteredItems.filter(
798
+ (req2) => req2.spec?.apiProductRef?.name === apiProductName
799
+ );
800
+ }
801
+ if (apiProductNamespace) {
802
+ filteredItems = filteredItems.filter(
803
+ (req2) => req2.spec?.apiProductRef?.namespace === apiProductNamespace
804
+ );
805
+ }
560
806
  res.json({ items: filteredItems });
561
807
  } catch (error) {
562
808
  console.error("error fetching user api key requests:", error);
@@ -584,50 +830,43 @@ async function createRouter({
584
830
  "devportal.kuadrant.io",
585
831
  "v1alpha1",
586
832
  namespace,
587
- "apikeys",
833
+ "apikeyrequests",
588
834
  name
589
835
  );
590
836
  const spec = request.spec;
591
837
  const apiProductName = spec.apiProductRef?.name;
592
838
  if (!apiProductName) {
593
- throw new errors.InputError("apiProductRef.name is required in APIKey spec");
839
+ throw new errors.InputError("apiProductRef.name is required in APIKeyRequest spec");
594
840
  }
595
- const apiProduct = await k8sClient$1.getCustomResource(
596
- "devportal.kuadrant.io",
597
- "v1alpha1",
841
+ await verifyApiKeyUpdatePermission(
842
+ credentials,
843
+ userEntityRef,
598
844
  namespace,
599
- "apiproducts",
600
- apiProductName
845
+ apiProductName,
846
+ k8sClient$1,
847
+ permissions$1
601
848
  );
602
- const owner = apiProduct.metadata?.annotations?.["backstage.io/owner"];
603
- const updateAllDecision = await permissions$1.authorize(
604
- [{ permission: permissions.kuadrantApiKeyUpdateAllPermission }],
605
- { credentials }
606
- );
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");
849
+ const approvalName = `${name}-approval-${crypto.randomBytes(4).toString("hex")}`;
850
+ const approval = {
851
+ apiVersion: "devportal.kuadrant.io/v1alpha1",
852
+ kind: "APIKeyApproval",
853
+ metadata: {
854
+ name: approvalName,
855
+ namespace
856
+ },
857
+ spec: {
858
+ apiKeyRequestRef: { name },
859
+ approved: true,
860
+ reviewedBy,
861
+ reviewedAt: (/* @__PURE__ */ new Date()).toISOString()
617
862
  }
618
- }
619
- const status = {
620
- phase: "Approved",
621
- reviewedBy,
622
- reviewedAt: (/* @__PURE__ */ new Date()).toISOString()
623
863
  };
624
- await k8sClient$1.patchCustomResourceStatus(
864
+ await k8sClient$1.createCustomResource(
625
865
  "devportal.kuadrant.io",
626
866
  "v1alpha1",
627
867
  namespace,
628
- "apikeys",
629
- name,
630
- status
868
+ "apikeyapprovals",
869
+ approval
631
870
  );
632
871
  res.json({ success: true });
633
872
  } catch (error) {
@@ -653,52 +892,45 @@ async function createRouter({
653
892
  "devportal.kuadrant.io",
654
893
  "v1alpha1",
655
894
  namespace,
656
- "apikeys",
895
+ "apikeyrequests",
657
896
  name
658
897
  );
659
898
  const spec = request.spec;
660
899
  const apiProductName = spec.apiProductRef?.name;
661
900
  if (!apiProductName) {
662
- throw new errors.InputError("apiProductRef.name is required in APIKey spec");
901
+ throw new errors.InputError("apiProductRef.name is required in APIKeyRequest spec");
663
902
  }
664
- const apiProduct = await k8sClient$1.getCustomResource(
665
- "devportal.kuadrant.io",
666
- "v1alpha1",
903
+ await verifyApiKeyUpdatePermission(
904
+ credentials,
905
+ userEntityRef,
667
906
  namespace,
668
- "apiproducts",
669
- apiProductName
907
+ apiProductName,
908
+ k8sClient$1,
909
+ permissions$1
670
910
  );
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");
911
+ const approvalName = `${name}-denial-${crypto.randomBytes(4).toString("hex")}`;
912
+ const approval = {
913
+ apiVersion: "devportal.kuadrant.io/v1alpha1",
914
+ kind: "APIKeyApproval",
915
+ metadata: {
916
+ name: approvalName,
917
+ namespace
918
+ },
919
+ spec: {
920
+ apiKeyRequestRef: { name },
921
+ approved: false,
922
+ reviewedBy,
923
+ reviewedAt: (/* @__PURE__ */ new Date()).toISOString()
686
924
  }
687
- }
688
- const status = {
689
- phase: "Rejected",
690
- reviewedBy,
691
- reviewedAt: (/* @__PURE__ */ new Date()).toISOString()
692
925
  };
693
- await k8sClient$1.patchCustomResourceStatus(
926
+ await k8sClient$1.createCustomResource(
694
927
  "devportal.kuadrant.io",
695
928
  "v1alpha1",
696
929
  namespace,
697
- "apikeys",
698
- name,
699
- status
930
+ "apikeyapprovals",
931
+ approval
700
932
  );
701
- res.status(204).send();
933
+ res.json({ success: true });
702
934
  } catch (error) {
703
935
  console.error("error rejecting api key request:", error);
704
936
  if (error instanceof errors.NotAllowedError) {
@@ -723,73 +955,52 @@ async function createRouter({
723
955
  try {
724
956
  const credentials = await httpAuth.credentials(req);
725
957
  const { userEntityRef } = await getUserIdentity(req, httpAuth, userInfo);
726
- const updateAllDecision = await permissions$1.authorize(
727
- [{ permission: permissions.kuadrantApiKeyUpdateAllPermission }],
728
- { credentials }
729
- );
730
- const canUpdateAll = updateAllDecision[0].result === pluginPermissionCommon.AuthorizeResult.ALLOW;
731
- if (!canUpdateAll) {
732
- const updateOwnDecision = await permissions$1.authorize(
733
- [{ permission: permissions.kuadrantApiKeyUpdateOwnPermission }],
734
- { credentials }
735
- );
736
- if (updateOwnDecision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
737
- throw new errors.NotAllowedError("unauthorised");
738
- }
739
- }
740
- const { requests } = parsed.data;
958
+ const { requests, comment } = parsed.data;
741
959
  const reviewedBy = userEntityRef;
742
960
  const results = [];
743
961
  for (const reqRef of requests) {
744
962
  try {
745
- if (!canUpdateAll) {
746
- const request = await k8sClient$1.getCustomResource(
747
- "devportal.kuadrant.io",
748
- "v1alpha1",
749
- reqRef.namespace,
750
- "apikeys",
751
- reqRef.name
752
- );
753
- const apiProductName = request.spec?.apiProductRef?.name;
754
- if (!apiProductName) {
755
- results.push({
756
- namespace: reqRef.namespace,
757
- name: reqRef.name,
758
- success: false,
759
- error: "API key has no associated API product."
760
- });
761
- continue;
762
- }
763
- const apiProduct = await k8sClient$1.getCustomResource(
764
- "devportal.kuadrant.io",
765
- "v1alpha1",
766
- reqRef.namespace,
767
- "apiproducts",
768
- apiProductName
769
- );
770
- const owner = apiProduct.metadata?.annotations?.["backstage.io/owner"];
771
- if (owner !== userEntityRef) {
772
- results.push({
773
- namespace: reqRef.namespace,
774
- name: reqRef.name,
775
- success: false,
776
- error: "You can only approve requests for your own API products."
777
- });
778
- continue;
779
- }
963
+ const request = await k8sClient$1.getCustomResource(
964
+ "devportal.kuadrant.io",
965
+ "v1alpha1",
966
+ reqRef.namespace,
967
+ "apikeyrequests",
968
+ reqRef.name
969
+ );
970
+ const apiProductName = request.spec?.apiProductRef?.name;
971
+ if (!apiProductName) {
972
+ throw new errors.InputError("apiProductRef.name is required in APIKeyRequest spec");
780
973
  }
781
- const status = {
782
- phase: "Approved",
783
- reviewedBy,
784
- reviewedAt: (/* @__PURE__ */ new Date()).toISOString()
974
+ await verifyApiKeyUpdatePermission(
975
+ credentials,
976
+ userEntityRef,
977
+ reqRef.namespace,
978
+ apiProductName,
979
+ k8sClient$1,
980
+ permissions$1
981
+ );
982
+ const approvalName = `${reqRef.name}-approval-${crypto.randomBytes(4).toString("hex")}`;
983
+ const approval = {
984
+ apiVersion: "devportal.kuadrant.io/v1alpha1",
985
+ kind: "APIKeyApproval",
986
+ metadata: {
987
+ name: approvalName,
988
+ namespace: reqRef.namespace
989
+ },
990
+ spec: {
991
+ apiKeyRequestRef: { name: reqRef.name },
992
+ approved: true,
993
+ reviewedBy,
994
+ reviewedAt: (/* @__PURE__ */ new Date()).toISOString(),
995
+ ...comment && { comment }
996
+ }
785
997
  };
786
- await k8sClient$1.patchCustomResourceStatus(
998
+ await k8sClient$1.createCustomResource(
787
999
  "devportal.kuadrant.io",
788
1000
  "v1alpha1",
789
1001
  reqRef.namespace,
790
- "apikeys",
791
- reqRef.name,
792
- status
1002
+ "apikeyapprovals",
1003
+ approval
793
1004
  );
794
1005
  results.push({ namespace: reqRef.namespace, name: reqRef.name, success: true });
795
1006
  } catch (error) {
@@ -820,21 +1031,7 @@ async function createRouter({
820
1031
  try {
821
1032
  const credentials = await httpAuth.credentials(req);
822
1033
  const { userEntityRef } = await getUserIdentity(req, httpAuth, userInfo);
823
- const updateAllDecision = await permissions$1.authorize(
824
- [{ permission: permissions.kuadrantApiKeyUpdateAllPermission }],
825
- { credentials }
826
- );
827
- const canUpdateAll = updateAllDecision[0].result === pluginPermissionCommon.AuthorizeResult.ALLOW;
828
- if (!canUpdateAll) {
829
- const updateOwnDecision = await permissions$1.authorize(
830
- [{ permission: permissions.kuadrantApiKeyUpdateOwnPermission }],
831
- { credentials }
832
- );
833
- if (updateOwnDecision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
834
- throw new errors.NotAllowedError("unauthorised");
835
- }
836
- }
837
- const { requests } = parsed.data;
1034
+ const { requests, comment } = parsed.data;
838
1035
  const reviewedBy = userEntityRef;
839
1036
  const results = [];
840
1037
  for (const reqRef of requests) {
@@ -843,39 +1040,43 @@ async function createRouter({
843
1040
  "devportal.kuadrant.io",
844
1041
  "v1alpha1",
845
1042
  reqRef.namespace,
846
- "apikeys",
1043
+ "apikeyrequests",
847
1044
  reqRef.name
848
1045
  );
849
- const spec = request.spec;
850
- const apiProduct = await k8sClient$1.getCustomResource(
851
- "devportal.kuadrant.io",
852
- "v1alpha1",
1046
+ const apiProductName = request.spec?.apiProductRef?.name;
1047
+ if (!apiProductName) {
1048
+ throw new errors.InputError("apiProductRef.name is required in APIKeyRequest spec");
1049
+ }
1050
+ await verifyApiKeyUpdatePermission(
1051
+ credentials,
1052
+ userEntityRef,
853
1053
  reqRef.namespace,
854
- "apiproducts",
855
- spec.apiProductRef?.name
1054
+ apiProductName,
1055
+ k8sClient$1,
1056
+ permissions$1
856
1057
  );
857
- const owner = apiProduct.metadata?.annotations?.["backstage.io/owner"];
858
- if (!canUpdateAll && owner !== userEntityRef) {
859
- results.push({
860
- namespace: reqRef.namespace,
861
- name: reqRef.name,
862
- success: false,
863
- error: "You can only reject requests for your own API products."
864
- });
865
- continue;
866
- }
867
- const status = {
868
- phase: "Rejected",
869
- reviewedBy,
870
- reviewedAt: (/* @__PURE__ */ new Date()).toISOString()
1058
+ const approvalName = `${reqRef.name}-denial-${crypto.randomBytes(4).toString("hex")}`;
1059
+ const approval = {
1060
+ apiVersion: "devportal.kuadrant.io/v1alpha1",
1061
+ kind: "APIKeyApproval",
1062
+ metadata: {
1063
+ name: approvalName,
1064
+ namespace: reqRef.namespace
1065
+ },
1066
+ spec: {
1067
+ apiKeyRequestRef: { name: reqRef.name },
1068
+ approved: false,
1069
+ reviewedBy,
1070
+ reviewedAt: (/* @__PURE__ */ new Date()).toISOString(),
1071
+ ...comment && { comment }
1072
+ }
871
1073
  };
872
- await k8sClient$1.patchCustomResourceStatus(
1074
+ await k8sClient$1.createCustomResource(
873
1075
  "devportal.kuadrant.io",
874
1076
  "v1alpha1",
875
1077
  reqRef.namespace,
876
- "apikeys",
877
- reqRef.name,
878
- status
1078
+ "apikeyapprovals",
1079
+ approval
879
1080
  );
880
1081
  results.push({ namespace: reqRef.namespace, name: reqRef.name, success: true });
881
1082
  } catch (error) {
@@ -903,30 +1104,23 @@ async function createRouter({
903
1104
  const credentials = await httpAuth.credentials(req);
904
1105
  const { userEntityRef } = await getUserIdentity(req, httpAuth, userInfo);
905
1106
  const { namespace, name } = req.params;
906
- const request = await k8sClient$1.getCustomResource(
1107
+ const apiKey = await k8sClient$1.getCustomResource(
907
1108
  "devportal.kuadrant.io",
908
1109
  "v1alpha1",
909
1110
  namespace,
910
1111
  "apikeys",
911
1112
  name
912
1113
  );
913
- const requestUserId = request.spec?.requestedBy?.userId;
914
- const deleteAllDecision = await permissions$1.authorize(
915
- [{ permission: permissions.kuadrantApiKeyDeleteAllPermission }],
1114
+ const deleteOwnDecision = await permissions$1.authorize(
1115
+ [{ permission: permissions.kuadrantApiKeyDeleteOwnPermission }],
916
1116
  { credentials }
917
1117
  );
918
- const canDeleteAll = deleteAllDecision[0].result === pluginPermissionCommon.AuthorizeResult.ALLOW;
919
- if (!canDeleteAll) {
920
- const deleteOwnDecision = await permissions$1.authorize(
921
- [{ permission: permissions.kuadrantApiKeyDeleteOwnPermission }],
922
- { credentials }
923
- );
924
- if (deleteOwnDecision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
925
- throw new errors.NotAllowedError("unauthorised");
926
- }
927
- if (requestUserId !== userEntityRef) {
928
- throw new errors.NotAllowedError("you can only delete your own api key requests");
929
- }
1118
+ if (deleteOwnDecision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
1119
+ throw new errors.NotAllowedError("unauthorised");
1120
+ }
1121
+ const requestUserId = apiKey.spec?.requestedBy?.userId;
1122
+ if (requestUserId !== userEntityRef) {
1123
+ throw new errors.NotAllowedError("you can only delete your own api key requests");
930
1124
  }
931
1125
  await k8sClient$1.deleteCustomResource(
932
1126
  "devportal.kuadrant.io",
@@ -968,7 +1162,21 @@ async function createRouter({
968
1162
  name
969
1163
  );
970
1164
  const requestUserId = existing.spec?.requestedBy?.userId;
971
- const currentPhase = existing.status?.phase || "Pending";
1165
+ let currentPhase = "Pending";
1166
+ const conditions = existing.status?.conditions;
1167
+ if (conditions && conditions.length > 0) {
1168
+ const approved = conditions.find(
1169
+ (c) => c.type === "Approved" && c.status === "True"
1170
+ );
1171
+ if (approved) {
1172
+ currentPhase = "Approved";
1173
+ } else {
1174
+ const denied = conditions.find(
1175
+ (c) => c.type === "Denied" && c.status === "True"
1176
+ );
1177
+ if (denied) currentPhase = "Denied";
1178
+ }
1179
+ }
972
1180
  if (currentPhase !== "Pending") {
973
1181
  throw new errors.NotAllowedError("only pending requests can be edited");
974
1182
  }
@@ -1086,19 +1294,13 @@ async function createRouter({
1086
1294
  throw new errors.NotAllowedError("you can only read your own api key secrets");
1087
1295
  }
1088
1296
  }
1089
- if (apiKey.status?.canReadSecret !== true) {
1090
- res.status(403).json({
1091
- error: "secret has already been read and cannot be retrieved again"
1092
- });
1093
- return;
1094
- }
1095
- if (!apiKey.status?.secretRef?.name || !apiKey.status?.secretRef?.key) {
1297
+ if (!apiKey.spec?.secretRef?.name) {
1096
1298
  res.status(404).json({
1097
- error: "secret reference not found in apikey status"
1299
+ error: "secretRef not found in APIKey spec"
1098
1300
  });
1099
1301
  return;
1100
1302
  }
1101
- const secretName = apiKey.status.secretRef.name;
1303
+ const secretName = apiKey.spec.secretRef.name;
1102
1304
  let secret;
1103
1305
  try {
1104
1306
  secret = await k8sClient$1.getSecret(namespace, secretName);
@@ -1118,17 +1320,6 @@ async function createRouter({
1118
1320
  return;
1119
1321
  }
1120
1322
  const decodedApiKey = Buffer.from(apiKeyValue, "base64").toString("utf-8");
1121
- await k8sClient$1.patchCustomResourceStatus(
1122
- "devportal.kuadrant.io",
1123
- "v1alpha1",
1124
- namespace,
1125
- "apikeys",
1126
- name,
1127
- {
1128
- ...apiKey.status,
1129
- canReadSecret: false
1130
- }
1131
- );
1132
1323
  res.json({
1133
1324
  apiKey: decodedApiKey
1134
1325
  });
@@ -1141,6 +1332,82 @@ async function createRouter({
1141
1332
  }
1142
1333
  }
1143
1334
  });
1335
+ router.get("/authpolicies", async (req, res) => {
1336
+ try {
1337
+ const credentials = await httpAuth.credentials(req);
1338
+ const decision = await permissions$1.authorize(
1339
+ [{ permission: permissions.kuadrantAuthPolicyListPermission }],
1340
+ { credentials }
1341
+ );
1342
+ if (decision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
1343
+ throw new errors.NotAllowedError("unauthorised");
1344
+ }
1345
+ const data = await k8sClient$1.listCustomResources("kuadrant.io", "v1", "authpolicies");
1346
+ const filtered = {
1347
+ items: (data.items || []).map((policy) => ({
1348
+ metadata: {
1349
+ name: policy.metadata.name,
1350
+ namespace: policy.metadata.namespace
1351
+ },
1352
+ spec: policy.spec ? {
1353
+ // expose targetRef to allow UI to match AuthPolicy -> HTTPRoute
1354
+ targetRef: policy.spec.targetRef ? {
1355
+ kind: policy.spec.targetRef.kind,
1356
+ name: policy.spec.targetRef.name,
1357
+ namespace: policy.spec.targetRef.namespace
1358
+ } : void 0
1359
+ } : {},
1360
+ status: policy.status
1361
+ }))
1362
+ };
1363
+ res.json(filtered);
1364
+ } catch (error) {
1365
+ console.error("error fetching authpolicies:", error);
1366
+ if (error instanceof errors.NotAllowedError) {
1367
+ res.status(403).json({ error: error.message });
1368
+ } else {
1369
+ res.status(500).json({ error: "failed to fetch authpolicies" });
1370
+ }
1371
+ }
1372
+ });
1373
+ router.get("/ratelimitpolicies", async (req, res) => {
1374
+ try {
1375
+ const credentials = await httpAuth.credentials(req);
1376
+ const decision = await permissions$1.authorize(
1377
+ [{ permission: permissions.kuadrantRateLimitPolicyListPermission }],
1378
+ { credentials }
1379
+ );
1380
+ if (decision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
1381
+ throw new errors.NotAllowedError("unauthorised");
1382
+ }
1383
+ const data = await k8sClient$1.listCustomResources("kuadrant.io", "v1", "ratelimitpolicies");
1384
+ const filtered = {
1385
+ items: (data.items || []).map((policy) => ({
1386
+ metadata: {
1387
+ name: policy.metadata.name,
1388
+ namespace: policy.metadata.namespace
1389
+ },
1390
+ spec: policy.spec ? {
1391
+ // expose targetRef to allow UI to match AuthPolicy -> HTTPRoute
1392
+ targetRef: policy.spec.targetRef ? {
1393
+ kind: policy.spec.targetRef.kind,
1394
+ name: policy.spec.targetRef.name,
1395
+ namespace: policy.spec.targetRef.namespace
1396
+ } : void 0
1397
+ } : {},
1398
+ status: policy.status
1399
+ }))
1400
+ };
1401
+ res.json(filtered);
1402
+ } catch (error) {
1403
+ console.error("error fetching ratelimitpolicies:", error);
1404
+ if (error instanceof errors.NotAllowedError) {
1405
+ res.status(403).json({ error: error.message });
1406
+ } else {
1407
+ res.status(500).json({ error: "failed to fetch ratelimitpolicies" });
1408
+ }
1409
+ }
1410
+ });
1144
1411
  router.use(pluginPermissionNode.createPermissionIntegrationRouter({
1145
1412
  permissions: permissions.kuadrantPermissions
1146
1413
  }));