@kuadrant/kuadrant-backstage-plugin-backend 0.2.1 → 0.3.0-aaa46b4
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.
- package/dist/k8s-client.cjs.js +16 -0
- package/dist/k8s-client.cjs.js.map +1 -1
- package/dist/router.cjs.js +324 -177
- package/dist/router.cjs.js.map +1 -1
- package/package.json +1 -1
package/dist/router.cjs.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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", "
|
|
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", "
|
|
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
|
-
|
|
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
|
|
780
|
+
const apiProductName = req.query.apiProductName;
|
|
781
|
+
const apiProductNamespace = req.query.apiProductNamespace;
|
|
782
|
+
const consumerNs = getConsumerNamespace(userEntityRef);
|
|
627
783
|
let data;
|
|
628
|
-
|
|
629
|
-
data = await k8sClient$1.listCustomResources("devportal.kuadrant.io", "v1alpha1", "apikeys",
|
|
630
|
-
}
|
|
631
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
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
|
|
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
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
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.
|
|
864
|
+
await k8sClient$1.createCustomResource(
|
|
685
865
|
"devportal.kuadrant.io",
|
|
686
866
|
"v1alpha1",
|
|
687
867
|
namespace,
|
|
688
|
-
"
|
|
689
|
-
|
|
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
|
-
"
|
|
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
|
|
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
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
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.
|
|
926
|
+
await k8sClient$1.createCustomResource(
|
|
738
927
|
"devportal.kuadrant.io",
|
|
739
928
|
"v1alpha1",
|
|
740
929
|
namespace,
|
|
741
|
-
"
|
|
742
|
-
|
|
743
|
-
status
|
|
930
|
+
"apikeyapprovals",
|
|
931
|
+
approval
|
|
744
932
|
);
|
|
745
|
-
res.
|
|
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
|
|
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
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
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
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
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.
|
|
998
|
+
await k8sClient$1.createCustomResource(
|
|
831
999
|
"devportal.kuadrant.io",
|
|
832
1000
|
"v1alpha1",
|
|
833
1001
|
reqRef.namespace,
|
|
834
|
-
"
|
|
835
|
-
|
|
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
|
|
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
|
-
"
|
|
1043
|
+
"apikeyrequests",
|
|
891
1044
|
reqRef.name
|
|
892
1045
|
);
|
|
893
|
-
const
|
|
894
|
-
|
|
895
|
-
"
|
|
896
|
-
|
|
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
|
-
|
|
899
|
-
|
|
1054
|
+
apiProductName,
|
|
1055
|
+
k8sClient$1,
|
|
1056
|
+
permissions$1
|
|
900
1057
|
);
|
|
901
|
-
const
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
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.
|
|
1074
|
+
await k8sClient$1.createCustomResource(
|
|
917
1075
|
"devportal.kuadrant.io",
|
|
918
1076
|
"v1alpha1",
|
|
919
1077
|
reqRef.namespace,
|
|
920
|
-
"
|
|
921
|
-
|
|
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
|
|
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
|
|
958
|
-
|
|
959
|
-
[{ permission: permissions.kuadrantApiKeyDeleteAllPermission }],
|
|
1114
|
+
const deleteOwnDecision = await permissions$1.authorize(
|
|
1115
|
+
[{ permission: permissions.kuadrantApiKeyDeleteOwnPermission }],
|
|
960
1116
|
{ credentials }
|
|
961
1117
|
);
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
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
|
-
|
|
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.
|
|
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: "
|
|
1299
|
+
error: "secretRef not found in APIKey spec"
|
|
1142
1300
|
});
|
|
1143
1301
|
return;
|
|
1144
1302
|
}
|
|
1145
|
-
const secretName = apiKey.
|
|
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
|
});
|