@kuadrant/kuadrant-backstage-plugin-backend 0.2.1 → 0.3.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.
@@ -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) {
@@ -233,8 +273,7 @@ async function createRouter({
233
273
  allRequests = await k8sClient$1.listCustomResources(
234
274
  "devportal.kuadrant.io",
235
275
  "v1alpha1",
236
- "apikeys",
237
- namespace
276
+ "apikeys"
238
277
  );
239
278
  } catch (error) {
240
279
  console.warn("failed to list apikeys during cascade delete:", error);
@@ -247,11 +286,12 @@ async function createRouter({
247
286
  const deletionResults = await Promise.allSettled(
248
287
  relatedRequests.map(async (request) => {
249
288
  const requestName = request.metadata.name;
250
- console.log(`deleting apikey: ${namespace}/${requestName}`);
289
+ const requestNamespace = request.metadata.namespace;
290
+ console.log(`deleting apikey: ${requestNamespace}/${requestName}`);
251
291
  await k8sClient$1.deleteCustomResource(
252
292
  "devportal.kuadrant.io",
253
293
  "v1alpha1",
254
- namespace,
294
+ requestNamespace,
255
295
  "apikeys",
256
296
  requestName
257
297
  );
@@ -489,14 +529,93 @@ async function createRouter({
489
529
  }
490
530
  }
491
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
+ });
492
609
  const requestSchema = zod.z.object({
493
610
  apiProductName: zod.z.string(),
494
611
  // name of the APIProduct
495
612
  namespace: zod.z.string(),
496
- // namespace where both APIProduct and APIKey live
613
+ // owner's namespace (where the APIProduct lives)
497
614
  planTier: zod.z.string(),
498
615
  useCase: zod.z.string().optional(),
499
- userEmail: zod.z.string().optional()
616
+ userEmail: zod.z.string().optional(),
617
+ secretName: zod.z.string()
618
+ // frontend creates secret via POST /secrets first
500
619
  });
501
620
  router.post("/requests", async (req, res) => {
502
621
  const parsed = requestSchema.safeParse(req.body);
@@ -505,7 +624,7 @@ async function createRouter({
505
624
  }
506
625
  try {
507
626
  const credentials = await httpAuth.credentials(req);
508
- const { apiProductName, namespace, planTier, useCase, userEmail } = parsed.data;
627
+ const { apiProductName, namespace, planTier, useCase, userEmail, secretName: frontendSecretName } = parsed.data;
509
628
  const { userEntityRef } = await getUserIdentity(req, httpAuth, userInfo);
510
629
  const resourceRef = `apiproduct:${namespace}/${apiProductName}`;
511
630
  const decision = await permissions$1.authorize(
@@ -518,23 +637,33 @@ async function createRouter({
518
637
  if (decision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
519
638
  throw new errors.NotAllowedError(`not authorised to request access to ${apiProductName}`);
520
639
  }
640
+ const consumerNamespace = getConsumerNamespace(userEntityRef);
641
+ await ensureConsumerNamespace(k8sClient$1, consumerNamespace);
521
642
  const randomSuffix = crypto.randomBytes(4).toString("hex");
522
- const userName = extractNameFromEntityRef(userEntityRef);
523
- const requestName = `${userName}-${apiProductName}-${randomSuffix}`.toLowerCase().replace(/[^a-z0-9-]/g, "-");
643
+ const requestName = `${apiProductName}-${randomSuffix}`.toLowerCase().replace(/[^a-z0-9-]/g, "-");
524
644
  const requestedBy = { userId: userEntityRef };
525
645
  if (userEmail) {
526
646
  requestedBy.email = userEmail;
527
647
  }
648
+ if (!frontendSecretName) {
649
+ throw new errors.InputError("secretName is required - create the secret via POST /secrets first");
650
+ }
651
+ const secretName = frontendSecretName;
528
652
  const request = {
529
653
  apiVersion: "devportal.kuadrant.io/v1alpha1",
530
654
  kind: "APIKey",
531
655
  metadata: {
532
656
  name: requestName,
533
- namespace
657
+ namespace: consumerNamespace
534
658
  },
535
659
  spec: {
536
660
  apiProductRef: {
537
- name: apiProductName
661
+ name: apiProductName,
662
+ namespace
663
+ // cross-namespace reference to owner's APIProduct
664
+ },
665
+ secretRef: {
666
+ name: secretName
538
667
  },
539
668
  planTier,
540
669
  useCase: useCase || "",
@@ -544,7 +673,7 @@ async function createRouter({
544
673
  const created = await k8sClient$1.createCustomResource(
545
674
  "devportal.kuadrant.io",
546
675
  "v1alpha1",
547
- namespace,
676
+ consumerNamespace,
548
677
  "apikeys",
549
678
  request
550
679
  );
@@ -578,11 +707,13 @@ async function createRouter({
578
707
  }
579
708
  const status = req.query.status;
580
709
  const namespace = req.query.namespace;
710
+ const apiProductName = req.query.apiProductName;
711
+ const apiProductNamespace = req.query.apiProductNamespace;
581
712
  let data;
582
713
  if (namespace) {
583
- data = await k8sClient$1.listCustomResources("devportal.kuadrant.io", "v1alpha1", "apikeys", namespace);
714
+ data = await k8sClient$1.listCustomResources("devportal.kuadrant.io", "v1alpha1", "apikeyrequests", namespace);
584
715
  } else {
585
- data = await k8sClient$1.listCustomResources("devportal.kuadrant.io", "v1alpha1", "apikeys");
716
+ data = await k8sClient$1.listCustomResources("devportal.kuadrant.io", "v1alpha1", "apikeyrequests");
586
717
  }
587
718
  let filteredItems = data.items || [];
588
719
  if (!canReadAll) {
@@ -596,9 +727,32 @@ async function createRouter({
596
727
  (req2) => ownedApiProducts.includes(req2.spec?.apiProductRef?.name)
597
728
  );
598
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
+ }
599
740
  if (status) {
600
741
  filteredItems = filteredItems.filter((req2) => {
601
- 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
+ }
602
756
  return phase === status;
603
757
  });
604
758
  }
@@ -623,16 +777,32 @@ async function createRouter({
623
777
  throw new errors.NotAllowedError("unauthorised");
624
778
  }
625
779
  const { userEntityRef } = await getUserIdentity(req, httpAuth, userInfo);
626
- const namespace = req.query.namespace;
780
+ const apiProductName = req.query.apiProductName;
781
+ const apiProductNamespace = req.query.apiProductNamespace;
782
+ const consumerNs = getConsumerNamespace(userEntityRef);
627
783
  let data;
628
- if (namespace) {
629
- data = await k8sClient$1.listCustomResources("devportal.kuadrant.io", "v1alpha1", "apikeys", namespace);
630
- } else {
631
- 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;
632
792
  }
633
- const filteredItems = (data.items || []).filter(
793
+ let filteredItems = (data.items || []).filter(
634
794
  (req2) => req2.spec?.requestedBy?.userId === userEntityRef
635
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
+ }
636
806
  res.json({ items: filteredItems });
637
807
  } catch (error) {
638
808
  console.error("error fetching user api key requests:", error);
@@ -660,13 +830,13 @@ async function createRouter({
660
830
  "devportal.kuadrant.io",
661
831
  "v1alpha1",
662
832
  namespace,
663
- "apikeys",
833
+ "apikeyrequests",
664
834
  name
665
835
  );
666
836
  const spec = request.spec;
667
837
  const apiProductName = spec.apiProductRef?.name;
668
838
  if (!apiProductName) {
669
- throw new errors.InputError("apiProductRef.name is required in APIKey spec");
839
+ throw new errors.InputError("apiProductRef.name is required in APIKeyRequest spec");
670
840
  }
671
841
  await verifyApiKeyUpdatePermission(
672
842
  credentials,
@@ -676,18 +846,27 @@ async function createRouter({
676
846
  k8sClient$1,
677
847
  permissions$1
678
848
  );
679
- const status = {
680
- phase: "Approved",
681
- reviewedBy,
682
- reviewedAt: (/* @__PURE__ */ new Date()).toISOString()
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()
862
+ }
683
863
  };
684
- await k8sClient$1.patchCustomResourceStatus(
864
+ await k8sClient$1.createCustomResource(
685
865
  "devportal.kuadrant.io",
686
866
  "v1alpha1",
687
867
  namespace,
688
- "apikeys",
689
- name,
690
- status
868
+ "apikeyapprovals",
869
+ approval
691
870
  );
692
871
  res.json({ success: true });
693
872
  } catch (error) {
@@ -713,13 +892,13 @@ async function createRouter({
713
892
  "devportal.kuadrant.io",
714
893
  "v1alpha1",
715
894
  namespace,
716
- "apikeys",
895
+ "apikeyrequests",
717
896
  name
718
897
  );
719
898
  const spec = request.spec;
720
899
  const apiProductName = spec.apiProductRef?.name;
721
900
  if (!apiProductName) {
722
- throw new errors.InputError("apiProductRef.name is required in APIKey spec");
901
+ throw new errors.InputError("apiProductRef.name is required in APIKeyRequest spec");
723
902
  }
724
903
  await verifyApiKeyUpdatePermission(
725
904
  credentials,
@@ -729,20 +908,29 @@ async function createRouter({
729
908
  k8sClient$1,
730
909
  permissions$1
731
910
  );
732
- const status = {
733
- phase: "Rejected",
734
- reviewedBy,
735
- reviewedAt: (/* @__PURE__ */ new Date()).toISOString()
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()
924
+ }
736
925
  };
737
- await k8sClient$1.patchCustomResourceStatus(
926
+ await k8sClient$1.createCustomResource(
738
927
  "devportal.kuadrant.io",
739
928
  "v1alpha1",
740
929
  namespace,
741
- "apikeys",
742
- name,
743
- status
930
+ "apikeyapprovals",
931
+ approval
744
932
  );
745
- res.status(204).send();
933
+ res.json({ success: true });
746
934
  } catch (error) {
747
935
  console.error("error rejecting api key request:", error);
748
936
  if (error instanceof errors.NotAllowedError) {
@@ -767,73 +955,52 @@ async function createRouter({
767
955
  try {
768
956
  const credentials = await httpAuth.credentials(req);
769
957
  const { userEntityRef } = await getUserIdentity(req, httpAuth, userInfo);
770
- const updateAllDecision = await permissions$1.authorize(
771
- [{ permission: permissions.kuadrantApiKeyUpdateAllPermission }],
772
- { credentials }
773
- );
774
- const canUpdateAll = updateAllDecision[0].result === pluginPermissionCommon.AuthorizeResult.ALLOW;
775
- if (!canUpdateAll) {
776
- const updateOwnDecision = await permissions$1.authorize(
777
- [{ permission: permissions.kuadrantApiKeyUpdateOwnPermission }],
778
- { credentials }
779
- );
780
- if (updateOwnDecision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
781
- throw new errors.NotAllowedError("unauthorised");
782
- }
783
- }
784
- const { requests } = parsed.data;
958
+ const { requests, comment } = parsed.data;
785
959
  const reviewedBy = userEntityRef;
786
960
  const results = [];
787
961
  for (const reqRef of requests) {
788
962
  try {
789
- if (!canUpdateAll) {
790
- const request = await k8sClient$1.getCustomResource(
791
- "devportal.kuadrant.io",
792
- "v1alpha1",
793
- reqRef.namespace,
794
- "apikeys",
795
- reqRef.name
796
- );
797
- const apiProductName = request.spec?.apiProductRef?.name;
798
- if (!apiProductName) {
799
- results.push({
800
- namespace: reqRef.namespace,
801
- name: reqRef.name,
802
- success: false,
803
- error: "API key has no associated API product."
804
- });
805
- continue;
806
- }
807
- const apiProduct = await k8sClient$1.getCustomResource(
808
- "devportal.kuadrant.io",
809
- "v1alpha1",
810
- reqRef.namespace,
811
- "apiproducts",
812
- apiProductName
813
- );
814
- const owner = apiProduct.metadata?.annotations?.["backstage.io/owner"];
815
- if (owner !== userEntityRef) {
816
- results.push({
817
- namespace: reqRef.namespace,
818
- name: reqRef.name,
819
- success: false,
820
- error: "You can only approve requests for your own API products."
821
- });
822
- continue;
823
- }
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");
824
973
  }
825
- const status = {
826
- phase: "Approved",
827
- reviewedBy,
828
- 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
+ }
829
997
  };
830
- await k8sClient$1.patchCustomResourceStatus(
998
+ await k8sClient$1.createCustomResource(
831
999
  "devportal.kuadrant.io",
832
1000
  "v1alpha1",
833
1001
  reqRef.namespace,
834
- "apikeys",
835
- reqRef.name,
836
- status
1002
+ "apikeyapprovals",
1003
+ approval
837
1004
  );
838
1005
  results.push({ namespace: reqRef.namespace, name: reqRef.name, success: true });
839
1006
  } catch (error) {
@@ -864,21 +1031,7 @@ async function createRouter({
864
1031
  try {
865
1032
  const credentials = await httpAuth.credentials(req);
866
1033
  const { userEntityRef } = await getUserIdentity(req, httpAuth, userInfo);
867
- const updateAllDecision = await permissions$1.authorize(
868
- [{ permission: permissions.kuadrantApiKeyUpdateAllPermission }],
869
- { credentials }
870
- );
871
- const canUpdateAll = updateAllDecision[0].result === pluginPermissionCommon.AuthorizeResult.ALLOW;
872
- if (!canUpdateAll) {
873
- const updateOwnDecision = await permissions$1.authorize(
874
- [{ permission: permissions.kuadrantApiKeyUpdateOwnPermission }],
875
- { credentials }
876
- );
877
- if (updateOwnDecision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
878
- throw new errors.NotAllowedError("unauthorised");
879
- }
880
- }
881
- const { requests } = parsed.data;
1034
+ const { requests, comment } = parsed.data;
882
1035
  const reviewedBy = userEntityRef;
883
1036
  const results = [];
884
1037
  for (const reqRef of requests) {
@@ -887,39 +1040,43 @@ async function createRouter({
887
1040
  "devportal.kuadrant.io",
888
1041
  "v1alpha1",
889
1042
  reqRef.namespace,
890
- "apikeys",
1043
+ "apikeyrequests",
891
1044
  reqRef.name
892
1045
  );
893
- const spec = request.spec;
894
- const apiProduct = await k8sClient$1.getCustomResource(
895
- "devportal.kuadrant.io",
896
- "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,
897
1053
  reqRef.namespace,
898
- "apiproducts",
899
- spec.apiProductRef?.name
1054
+ apiProductName,
1055
+ k8sClient$1,
1056
+ permissions$1
900
1057
  );
901
- const owner = apiProduct.metadata?.annotations?.["backstage.io/owner"];
902
- if (!canUpdateAll && owner !== userEntityRef) {
903
- results.push({
904
- namespace: reqRef.namespace,
905
- name: reqRef.name,
906
- success: false,
907
- error: "You can only reject requests for your own API products."
908
- });
909
- continue;
910
- }
911
- const status = {
912
- phase: "Rejected",
913
- reviewedBy,
914
- 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
+ }
915
1073
  };
916
- await k8sClient$1.patchCustomResourceStatus(
1074
+ await k8sClient$1.createCustomResource(
917
1075
  "devportal.kuadrant.io",
918
1076
  "v1alpha1",
919
1077
  reqRef.namespace,
920
- "apikeys",
921
- reqRef.name,
922
- status
1078
+ "apikeyapprovals",
1079
+ approval
923
1080
  );
924
1081
  results.push({ namespace: reqRef.namespace, name: reqRef.name, success: true });
925
1082
  } catch (error) {
@@ -947,30 +1104,23 @@ async function createRouter({
947
1104
  const credentials = await httpAuth.credentials(req);
948
1105
  const { userEntityRef } = await getUserIdentity(req, httpAuth, userInfo);
949
1106
  const { namespace, name } = req.params;
950
- const request = await k8sClient$1.getCustomResource(
1107
+ const apiKey = await k8sClient$1.getCustomResource(
951
1108
  "devportal.kuadrant.io",
952
1109
  "v1alpha1",
953
1110
  namespace,
954
1111
  "apikeys",
955
1112
  name
956
1113
  );
957
- const requestUserId = request.spec?.requestedBy?.userId;
958
- const deleteAllDecision = await permissions$1.authorize(
959
- [{ permission: permissions.kuadrantApiKeyDeleteAllPermission }],
1114
+ const deleteOwnDecision = await permissions$1.authorize(
1115
+ [{ permission: permissions.kuadrantApiKeyDeleteOwnPermission }],
960
1116
  { credentials }
961
1117
  );
962
- const canDeleteAll = deleteAllDecision[0].result === pluginPermissionCommon.AuthorizeResult.ALLOW;
963
- if (!canDeleteAll) {
964
- const deleteOwnDecision = await permissions$1.authorize(
965
- [{ permission: permissions.kuadrantApiKeyDeleteOwnPermission }],
966
- { credentials }
967
- );
968
- if (deleteOwnDecision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
969
- throw new errors.NotAllowedError("unauthorised");
970
- }
971
- if (requestUserId !== userEntityRef) {
972
- throw new errors.NotAllowedError("you can only delete your own api key requests");
973
- }
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");
974
1124
  }
975
1125
  await k8sClient$1.deleteCustomResource(
976
1126
  "devportal.kuadrant.io",
@@ -1012,7 +1162,21 @@ async function createRouter({
1012
1162
  name
1013
1163
  );
1014
1164
  const requestUserId = existing.spec?.requestedBy?.userId;
1015
- 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
+ }
1016
1180
  if (currentPhase !== "Pending") {
1017
1181
  throw new errors.NotAllowedError("only pending requests can be edited");
1018
1182
  }
@@ -1130,19 +1294,13 @@ async function createRouter({
1130
1294
  throw new errors.NotAllowedError("you can only read your own api key secrets");
1131
1295
  }
1132
1296
  }
1133
- if (apiKey.status?.canReadSecret !== true) {
1134
- res.status(403).json({
1135
- error: "secret has already been read and cannot be retrieved again"
1136
- });
1137
- return;
1138
- }
1139
- if (!apiKey.status?.secretRef?.name || !apiKey.status?.secretRef?.key) {
1297
+ if (!apiKey.spec?.secretRef?.name) {
1140
1298
  res.status(404).json({
1141
- error: "secret reference not found in apikey status"
1299
+ error: "secretRef not found in APIKey spec"
1142
1300
  });
1143
1301
  return;
1144
1302
  }
1145
- const secretName = apiKey.status.secretRef.name;
1303
+ const secretName = apiKey.spec.secretRef.name;
1146
1304
  let secret;
1147
1305
  try {
1148
1306
  secret = await k8sClient$1.getSecret(namespace, secretName);
@@ -1162,17 +1320,6 @@ async function createRouter({
1162
1320
  return;
1163
1321
  }
1164
1322
  const decodedApiKey = Buffer.from(apiKeyValue, "base64").toString("utf-8");
1165
- await k8sClient$1.patchCustomResourceStatus(
1166
- "devportal.kuadrant.io",
1167
- "v1alpha1",
1168
- namespace,
1169
- "apikeys",
1170
- name,
1171
- {
1172
- ...apiKey.status,
1173
- canReadSecret: false
1174
- }
1175
- );
1176
1323
  res.json({
1177
1324
  apiKey: decodedApiKey
1178
1325
  });