@kuadrant/kuadrant-backstage-plugin-backend 0.0.1-test.1-1593c3ec

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.
@@ -0,0 +1,820 @@
1
+ 'use strict';
2
+
3
+ var errors = require('@backstage/errors');
4
+ var pluginPermissionCommon = require('@backstage/plugin-permission-common');
5
+ var pluginPermissionNode = require('@backstage/plugin-permission-node');
6
+ var zod = require('zod');
7
+ var express = require('express');
8
+ var Router = require('express-promise-router');
9
+ var cors = require('cors');
10
+ var crypto = require('crypto');
11
+ var k8sClient = require('./k8s-client.cjs.js');
12
+ var permissions = require('./permissions.cjs.js');
13
+
14
+ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
15
+
16
+ var express__default = /*#__PURE__*/_interopDefaultCompat(express);
17
+ var Router__default = /*#__PURE__*/_interopDefaultCompat(Router);
18
+ var cors__default = /*#__PURE__*/_interopDefaultCompat(cors);
19
+
20
+ function generateApiKey() {
21
+ return crypto.randomBytes(32).toString("hex");
22
+ }
23
+ async function getUserIdentity(req, httpAuth, userInfo) {
24
+ try {
25
+ const credentials = await httpAuth.credentials(req, { allow: ["user", "none"] });
26
+ if (!credentials || !credentials.principal || credentials.principal.type === "none") {
27
+ console.log("no user credentials, treating as guest api owner");
28
+ return {
29
+ userId: "guest",
30
+ isPlatformEngineer: false,
31
+ isApiOwner: true,
32
+ // allow guest as api owner in development
33
+ isApiConsumer: true,
34
+ groups: []
35
+ };
36
+ }
37
+ const info = await userInfo.getUserInfo(credentials);
38
+ const userId = info.userEntityRef.split("/")[1] || "guest";
39
+ const groups = info.ownershipEntityRefs || [];
40
+ const isPlatformEngineer = userId === "guest" || groups.some(
41
+ (ref) => ref === "group:default/platform-engineers" || ref === "group:default/platform-admins"
42
+ );
43
+ const isApiOwner = userId === "guest" || groups.some(
44
+ (ref) => ref === "group:default/api-owners" || ref === "group:default/app-developers"
45
+ );
46
+ const isApiConsumer = groups.some(
47
+ (ref) => ref === "group:default/api-consumers"
48
+ );
49
+ console.log(`user identity resolved: userId=${userId}, isPlatformEngineer=${isPlatformEngineer}, isApiOwner=${isApiOwner}, isApiConsumer=${isApiConsumer}, groups=${groups.join(",")}`);
50
+ return { userId, isPlatformEngineer, isApiOwner, isApiConsumer, groups };
51
+ } catch (error) {
52
+ const errorMsg = error instanceof Error ? error.message : String(error);
53
+ console.warn(`failed to get user identity, defaulting to guest api owner: ${errorMsg}`);
54
+ return {
55
+ userId: "guest",
56
+ isPlatformEngineer: false,
57
+ isApiOwner: true,
58
+ // allow guest as api owner in development
59
+ isApiConsumer: true,
60
+ groups: []
61
+ };
62
+ }
63
+ }
64
+ async function createRouter({
65
+ httpAuth,
66
+ userInfo,
67
+ config,
68
+ permissions: permissions$1
69
+ }) {
70
+ const router = Router__default.default();
71
+ router.use(cors__default.default({
72
+ origin: "http://localhost:3000",
73
+ credentials: true
74
+ }));
75
+ router.use(express__default.default.json());
76
+ const k8sClient$1 = new k8sClient.KuadrantK8sClient(config);
77
+ router.get("/apiproducts", async (req, res) => {
78
+ try {
79
+ const credentials = await httpAuth.credentials(req);
80
+ const decision = await permissions$1.authorize(
81
+ [{ permission: permissions.kuadrantApiProductListPermission }],
82
+ { credentials }
83
+ );
84
+ if (decision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
85
+ throw new errors.NotAllowedError("unauthorised");
86
+ }
87
+ const data = await k8sClient$1.listCustomResources("extensions.kuadrant.io", "v1alpha1", "apiproducts");
88
+ res.json(data);
89
+ } catch (error) {
90
+ console.error("error fetching apiproducts:", error);
91
+ if (error instanceof errors.NotAllowedError) {
92
+ res.status(403).json({ error: error.message });
93
+ } else {
94
+ res.status(500).json({ error: "failed to fetch apiproducts" });
95
+ }
96
+ }
97
+ });
98
+ router.get("/apiproducts/:namespace/:name", async (req, res) => {
99
+ try {
100
+ const credentials = await httpAuth.credentials(req);
101
+ const decision = await permissions$1.authorize(
102
+ [{ permission: permissions.kuadrantApiProductReadPermission }],
103
+ { credentials }
104
+ );
105
+ if (decision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
106
+ throw new errors.NotAllowedError("unauthorised");
107
+ }
108
+ const { namespace, name } = req.params;
109
+ const data = await k8sClient$1.getCustomResource("extensions.kuadrant.io", "v1alpha1", namespace, "apiproducts", name);
110
+ res.json(data);
111
+ } catch (error) {
112
+ console.error("error fetching apiproduct:", error);
113
+ if (error instanceof errors.NotAllowedError) {
114
+ res.status(403).json({ error: error.message });
115
+ } else {
116
+ res.status(500).json({ error: "failed to fetch apiproduct" });
117
+ }
118
+ }
119
+ });
120
+ router.post("/apiproducts", async (req, res) => {
121
+ try {
122
+ const credentials = await httpAuth.credentials(req);
123
+ const decision = await permissions$1.authorize(
124
+ [{ permission: permissions.kuadrantApiProductCreatePermission }],
125
+ { credentials }
126
+ );
127
+ if (decision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
128
+ throw new errors.NotAllowedError("unauthorised");
129
+ }
130
+ const { userId } = await getUserIdentity(req, httpAuth, userInfo);
131
+ const apiProduct = req.body;
132
+ const namespace = apiProduct.metadata?.namespace;
133
+ const planPolicyRef = apiProduct.spec?.planPolicyRef;
134
+ if (!namespace) {
135
+ throw new errors.InputError("namespace is required in metadata");
136
+ }
137
+ if (!planPolicyRef?.name || !planPolicyRef?.namespace) {
138
+ throw new errors.InputError("planPolicyRef with name and namespace is required");
139
+ }
140
+ const planPolicy = await k8sClient$1.getCustomResource(
141
+ "extensions.kuadrant.io",
142
+ "v1alpha1",
143
+ planPolicyRef.namespace,
144
+ "planpolicies",
145
+ planPolicyRef.name
146
+ );
147
+ const plans = planPolicy.spec?.plans || [];
148
+ if (plans.length === 0) {
149
+ throw new errors.InputError("selected planpolicy has no plans defined");
150
+ }
151
+ apiProduct.spec.plans = plans;
152
+ if (!apiProduct.spec.contact) {
153
+ apiProduct.spec.contact = {};
154
+ }
155
+ apiProduct.spec.contact.team = `user:default/${userId}`;
156
+ const created = await k8sClient$1.createCustomResource(
157
+ "extensions.kuadrant.io",
158
+ "v1alpha1",
159
+ namespace,
160
+ "apiproducts",
161
+ apiProduct
162
+ );
163
+ res.status(201).json(created);
164
+ } catch (error) {
165
+ console.error("error creating apiproduct:", error);
166
+ const errorMessage = error instanceof Error ? error.message : String(error);
167
+ if (error instanceof errors.NotAllowedError) {
168
+ res.status(403).json({ error: error.message });
169
+ } else if (error instanceof errors.InputError) {
170
+ res.status(400).json({ error: error.message });
171
+ } else {
172
+ res.status(500).json({ error: errorMessage });
173
+ }
174
+ }
175
+ });
176
+ router.delete("/apiproducts/:namespace/:name", async (req, res) => {
177
+ try {
178
+ const credentials = await httpAuth.credentials(req);
179
+ const decision = await permissions$1.authorize(
180
+ [{ permission: permissions.kuadrantApiProductDeletePermission }],
181
+ { credentials }
182
+ );
183
+ if (decision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
184
+ throw new errors.NotAllowedError("unauthorised");
185
+ }
186
+ const { namespace, name } = req.params;
187
+ await k8sClient$1.deleteCustomResource(
188
+ "extensions.kuadrant.io",
189
+ "v1alpha1",
190
+ namespace,
191
+ "apiproducts",
192
+ name
193
+ );
194
+ res.status(204).send();
195
+ } catch (error) {
196
+ console.error("error deleting apiproduct:", error);
197
+ if (error instanceof errors.NotAllowedError) {
198
+ res.status(403).json({ error: error.message });
199
+ } else {
200
+ res.status(500).json({ error: "failed to delete apiproduct" });
201
+ }
202
+ }
203
+ });
204
+ router.get("/planpolicies", async (req, res) => {
205
+ try {
206
+ const credentials = await httpAuth.credentials(req);
207
+ const decision = await permissions$1.authorize(
208
+ [{ permission: permissions.kuadrantPlanPolicyListPermission }],
209
+ { credentials }
210
+ );
211
+ if (decision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
212
+ throw new errors.NotAllowedError("unauthorised");
213
+ }
214
+ const data = await k8sClient$1.listCustomResources("extensions.kuadrant.io", "v1alpha1", "planpolicies");
215
+ const filtered = {
216
+ items: (data.items || []).map((policy) => ({
217
+ metadata: {
218
+ name: policy.metadata.name,
219
+ namespace: policy.metadata.namespace
220
+ }
221
+ }))
222
+ };
223
+ res.json(filtered);
224
+ } catch (error) {
225
+ console.error("error fetching planpolicies:", error);
226
+ if (error instanceof errors.NotAllowedError) {
227
+ res.status(403).json({ error: error.message });
228
+ } else {
229
+ res.status(500).json({ error: "failed to fetch planpolicies" });
230
+ }
231
+ }
232
+ });
233
+ router.get("/planpolicies/:namespace/:name", async (req, res) => {
234
+ try {
235
+ const credentials = await httpAuth.credentials(req);
236
+ const decision = await permissions$1.authorize(
237
+ [{ permission: permissions.kuadrantPlanPolicyReadPermission }],
238
+ { credentials }
239
+ );
240
+ if (decision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
241
+ throw new errors.NotAllowedError("unauthorised");
242
+ }
243
+ const { namespace, name } = req.params;
244
+ const data = await k8sClient$1.getCustomResource("extensions.kuadrant.io", "v1alpha1", namespace, "planpolicies", name);
245
+ res.json(data);
246
+ } catch (error) {
247
+ console.error("error fetching planpolicy:", error);
248
+ if (error instanceof errors.NotAllowedError) {
249
+ res.status(403).json({ error: error.message });
250
+ } else {
251
+ res.status(500).json({ error: "failed to fetch planpolicy" });
252
+ }
253
+ }
254
+ });
255
+ router.get("/apikeys", async (req, res) => {
256
+ try {
257
+ const credentials = await httpAuth.credentials(req);
258
+ const userId = req.query.userId;
259
+ const namespace = req.query.namespace;
260
+ if (!namespace) {
261
+ throw new errors.InputError("namespace query parameter is required");
262
+ }
263
+ const permission = userId ? permissions.kuadrantApiKeyReadOwnPermission : permissions.kuadrantApiKeyReadAllPermission;
264
+ const decision = await permissions$1.authorize(
265
+ [{ permission }],
266
+ { credentials }
267
+ );
268
+ if (decision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
269
+ throw new errors.NotAllowedError("unauthorised");
270
+ }
271
+ const data = await k8sClient$1.listSecrets(namespace);
272
+ let filteredItems = data.items || [];
273
+ if (userId) {
274
+ filteredItems = filteredItems.filter(
275
+ (secret) => secret.metadata?.annotations?.["secret.kuadrant.io/user-id"] === userId
276
+ );
277
+ }
278
+ filteredItems = filteredItems.filter(
279
+ (secret) => secret.metadata?.annotations?.["secret.kuadrant.io/user-id"]
280
+ );
281
+ res.json({ items: filteredItems });
282
+ } catch (error) {
283
+ console.error("error fetching api keys:", error);
284
+ if (error instanceof errors.NotAllowedError) {
285
+ res.status(403).json({ error: error.message });
286
+ } else {
287
+ res.status(500).json({ error: "failed to fetch api keys" });
288
+ }
289
+ }
290
+ });
291
+ router.delete("/apikeys/:namespace/:name", async (req, res) => {
292
+ try {
293
+ const credentials = await httpAuth.credentials(req);
294
+ const { userId } = await getUserIdentity(req, httpAuth, userInfo);
295
+ const { namespace, name } = req.params;
296
+ const secret = await k8sClient$1.getSecret(namespace, name);
297
+ const secretUserId = secret.metadata?.annotations?.["secret.kuadrant.io/user-id"];
298
+ const deleteAllDecision = await permissions$1.authorize(
299
+ [{ permission: permissions.kuadrantApiKeyDeleteAllPermission }],
300
+ { credentials }
301
+ );
302
+ const canDeleteAll = deleteAllDecision[0].result === pluginPermissionCommon.AuthorizeResult.ALLOW;
303
+ if (!canDeleteAll) {
304
+ const deleteOwnDecision = await permissions$1.authorize(
305
+ [{ permission: permissions.kuadrantApiKeyDeleteOwnPermission }],
306
+ { credentials }
307
+ );
308
+ if (deleteOwnDecision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
309
+ throw new errors.NotAllowedError("unauthorised");
310
+ }
311
+ if (secretUserId !== userId) {
312
+ throw new errors.NotAllowedError("you can only delete your own api keys");
313
+ }
314
+ }
315
+ await k8sClient$1.deleteSecret(namespace, name);
316
+ res.status(204).send();
317
+ } catch (error) {
318
+ console.error("error deleting api key:", error);
319
+ if (error instanceof errors.NotAllowedError) {
320
+ res.status(403).json({ error: error.message });
321
+ } else {
322
+ res.status(500).json({ error: "failed to delete api key" });
323
+ }
324
+ }
325
+ });
326
+ const requestSchema = zod.z.object({
327
+ apiName: zod.z.string(),
328
+ apiNamespace: zod.z.string(),
329
+ planTier: zod.z.string(),
330
+ useCase: zod.z.string().optional(),
331
+ userId: zod.z.string(),
332
+ userEmail: zod.z.string().optional(),
333
+ namespace: zod.z.string()
334
+ });
335
+ router.post("/requests", async (req, res) => {
336
+ const parsed = requestSchema.safeParse(req.body);
337
+ if (!parsed.success) {
338
+ throw new errors.InputError(parsed.error.toString());
339
+ }
340
+ try {
341
+ const credentials = await httpAuth.credentials(req);
342
+ const { apiName, apiNamespace, planTier, useCase, userId, userEmail, namespace } = parsed.data;
343
+ const resourceRef = `apiproduct:${apiNamespace}/${apiName}`;
344
+ const decision = await permissions$1.authorize(
345
+ [{
346
+ permission: permissions.kuadrantApiKeyRequestCreatePermission,
347
+ resourceRef
348
+ }],
349
+ { credentials }
350
+ );
351
+ if (decision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
352
+ throw new errors.NotAllowedError(`not authorised to request access to ${apiName}`);
353
+ }
354
+ const { userId: authenticatedUserId, isPlatformEngineer, isApiOwner } = await getUserIdentity(req, httpAuth, userInfo);
355
+ const canCreateForOthers = isPlatformEngineer || isApiOwner;
356
+ if (!canCreateForOthers && userId !== authenticatedUserId) {
357
+ throw new errors.NotAllowedError("you can only create api key requests for yourself");
358
+ }
359
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
360
+ const randomSuffix = crypto.randomBytes(4).toString("hex");
361
+ const requestName = `${userId}-${apiName}-${randomSuffix}`.toLowerCase().replace(/[^a-z0-9-]/g, "-");
362
+ const requestedBy = { userId };
363
+ if (userEmail) {
364
+ requestedBy.email = userEmail;
365
+ }
366
+ const request = {
367
+ apiVersion: "extensions.kuadrant.io/v1alpha1",
368
+ kind: "APIKeyRequest",
369
+ metadata: {
370
+ name: requestName,
371
+ namespace
372
+ },
373
+ spec: {
374
+ apiName,
375
+ apiNamespace,
376
+ planTier,
377
+ useCase: useCase || "",
378
+ requestedBy,
379
+ requestedAt: timestamp
380
+ }
381
+ };
382
+ const created = await k8sClient$1.createCustomResource(
383
+ "extensions.kuadrant.io",
384
+ "v1alpha1",
385
+ namespace,
386
+ "apikeyrequests",
387
+ request
388
+ );
389
+ try {
390
+ const apiProduct = await k8sClient$1.getCustomResource(
391
+ "extensions.kuadrant.io",
392
+ "v1alpha1",
393
+ apiNamespace,
394
+ "apiproducts",
395
+ apiName
396
+ );
397
+ if (apiProduct.spec?.approvalMode === "automatic") {
398
+ const apiKey = generateApiKey();
399
+ const timestamp2 = Date.now();
400
+ const secretName = `${userId}-${apiName}-${timestamp2}`.toLowerCase().replace(/[^a-z0-9-]/g, "-");
401
+ const secret = {
402
+ apiVersion: "v1",
403
+ kind: "Secret",
404
+ metadata: {
405
+ name: secretName,
406
+ namespace: apiNamespace,
407
+ labels: {
408
+ app: apiName
409
+ },
410
+ annotations: {
411
+ "secret.kuadrant.io/plan-id": planTier,
412
+ "secret.kuadrant.io/user-id": userId
413
+ }
414
+ },
415
+ stringData: {
416
+ api_key: apiKey
417
+ },
418
+ type: "Opaque"
419
+ };
420
+ await k8sClient$1.createSecret(apiNamespace, secret);
421
+ let planLimits = null;
422
+ const plan = apiProduct.spec?.plans?.find((p) => p.tier === planTier);
423
+ if (plan) {
424
+ planLimits = plan.limits;
425
+ }
426
+ let apiHostname = `${apiName}.apps.example.com`;
427
+ try {
428
+ const httproute = await k8sClient$1.getCustomResource(
429
+ "gateway.networking.k8s.io",
430
+ "v1",
431
+ apiNamespace,
432
+ "httproutes",
433
+ apiName
434
+ );
435
+ if (httproute.spec?.hostnames && httproute.spec.hostnames.length > 0) {
436
+ apiHostname = httproute.spec.hostnames[0];
437
+ }
438
+ } catch (error) {
439
+ console.warn("could not fetch httproute for hostname, using default:", error);
440
+ }
441
+ const status = {
442
+ phase: "Approved",
443
+ reviewedBy: "system",
444
+ reviewedAt: (/* @__PURE__ */ new Date()).toISOString(),
445
+ reason: "automatic approval",
446
+ apiKey,
447
+ apiHostname,
448
+ apiBasePath: "/api/v1",
449
+ apiDescription: `${apiName} api`,
450
+ planLimits
451
+ };
452
+ await k8sClient$1.patchCustomResourceStatus(
453
+ "extensions.kuadrant.io",
454
+ "v1alpha1",
455
+ namespace,
456
+ "apikeyrequests",
457
+ requestName,
458
+ status
459
+ );
460
+ }
461
+ } catch (error) {
462
+ console.warn("could not check approval mode or auto-approve:", error);
463
+ }
464
+ res.status(201).json(created);
465
+ } catch (error) {
466
+ console.error("error creating api key request:", error);
467
+ if (error instanceof errors.NotAllowedError) {
468
+ res.status(403).json({ error: error.message });
469
+ } else {
470
+ res.status(500).json({ error: "failed to create api key request" });
471
+ }
472
+ }
473
+ });
474
+ router.get("/requests", async (req, res) => {
475
+ try {
476
+ const credentials = await httpAuth.credentials(req);
477
+ const decision = await permissions$1.authorize(
478
+ [{ permission: permissions.kuadrantApiKeyRequestListPermission }],
479
+ { credentials }
480
+ );
481
+ if (decision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
482
+ throw new errors.NotAllowedError("unauthorised");
483
+ }
484
+ const status = req.query.status;
485
+ const namespace = req.query.namespace;
486
+ let data;
487
+ if (namespace) {
488
+ data = await k8sClient$1.listCustomResources("extensions.kuadrant.io", "v1alpha1", "apikeyrequests", namespace);
489
+ } else {
490
+ data = await k8sClient$1.listCustomResources("extensions.kuadrant.io", "v1alpha1", "apikeyrequests");
491
+ }
492
+ let filteredItems = data.items || [];
493
+ if (status) {
494
+ filteredItems = filteredItems.filter((req2) => {
495
+ const phase = req2.status?.phase || "Pending";
496
+ return phase === status;
497
+ });
498
+ }
499
+ res.json({ items: filteredItems });
500
+ } catch (error) {
501
+ console.error("error fetching api key requests:", error);
502
+ if (error instanceof errors.NotAllowedError) {
503
+ res.status(403).json({ error: error.message });
504
+ } else {
505
+ res.status(500).json({ error: "failed to fetch api key requests" });
506
+ }
507
+ }
508
+ });
509
+ router.get("/requests/my", async (req, res) => {
510
+ try {
511
+ const credentials = await httpAuth.credentials(req);
512
+ const decision = await permissions$1.authorize(
513
+ [{ permission: permissions.kuadrantApiKeyRequestReadOwnPermission }],
514
+ { credentials }
515
+ );
516
+ if (decision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
517
+ throw new errors.NotAllowedError("unauthorised");
518
+ }
519
+ const userId = req.query.userId;
520
+ const namespace = req.query.namespace;
521
+ if (!userId) {
522
+ throw new errors.InputError("userId query parameter is required");
523
+ }
524
+ let data;
525
+ if (namespace) {
526
+ data = await k8sClient$1.listCustomResources("extensions.kuadrant.io", "v1alpha1", "apikeyrequests", namespace);
527
+ } else {
528
+ data = await k8sClient$1.listCustomResources("extensions.kuadrant.io", "v1alpha1", "apikeyrequests");
529
+ }
530
+ const filteredItems = (data.items || []).filter(
531
+ (req2) => req2.spec?.requestedBy?.userId === userId
532
+ );
533
+ res.json({ items: filteredItems });
534
+ } catch (error) {
535
+ console.error("error fetching user api key requests:", error);
536
+ if (error instanceof errors.NotAllowedError) {
537
+ res.status(403).json({ error: error.message });
538
+ } else {
539
+ res.status(500).json({ error: "failed to fetch user api key requests" });
540
+ }
541
+ }
542
+ });
543
+ const approveRejectSchema = zod.z.object({
544
+ comment: zod.z.string().optional()
545
+ });
546
+ router.post("/requests/:namespace/:name/approve", async (req, res) => {
547
+ const parsed = approveRejectSchema.safeParse(req.body);
548
+ if (!parsed.success) {
549
+ throw new errors.InputError(parsed.error.toString());
550
+ }
551
+ try {
552
+ const { userId, isApiOwner } = await getUserIdentity(req, httpAuth, userInfo);
553
+ let canApprove = isApiOwner;
554
+ if (!canApprove) {
555
+ try {
556
+ const credentials = await httpAuth.credentials(req, { allow: ["none"] });
557
+ if (credentials) {
558
+ const decision = await permissions$1.authorize(
559
+ [{ permission: permissions.kuadrantApiKeyRequestUpdatePermission }],
560
+ { credentials }
561
+ );
562
+ canApprove = decision[0].result === pluginPermissionCommon.AuthorizeResult.ALLOW;
563
+ }
564
+ } catch (error) {
565
+ console.warn("permission check failed, using group-based authorization:", error);
566
+ }
567
+ }
568
+ if (!canApprove) {
569
+ throw new errors.NotAllowedError("you do not have permission to approve api key requests");
570
+ }
571
+ const { namespace, name } = req.params;
572
+ const { comment } = parsed.data;
573
+ const reviewedBy = `user:default/${userId}`;
574
+ const request = await k8sClient$1.getCustomResource(
575
+ "extensions.kuadrant.io",
576
+ "v1alpha1",
577
+ namespace,
578
+ "apikeyrequests",
579
+ name
580
+ );
581
+ const spec = request.spec;
582
+ const apiKey = generateApiKey();
583
+ const timestamp = Date.now();
584
+ const secretName = `${spec.requestedBy.userId}-${spec.apiName}-${timestamp}`.toLowerCase().replace(/[^a-z0-9-]/g, "-");
585
+ const secret = {
586
+ apiVersion: "v1",
587
+ kind: "Secret",
588
+ metadata: {
589
+ name: secretName,
590
+ namespace: spec.apiNamespace,
591
+ labels: {
592
+ app: spec.apiName
593
+ },
594
+ annotations: {
595
+ "secret.kuadrant.io/plan-id": spec.planTier,
596
+ "secret.kuadrant.io/user-id": spec.requestedBy.userId
597
+ }
598
+ },
599
+ stringData: {
600
+ api_key: apiKey
601
+ },
602
+ type: "Opaque"
603
+ };
604
+ await k8sClient$1.createSecret(spec.apiNamespace, secret);
605
+ let planLimits = null;
606
+ try {
607
+ const products = await k8sClient$1.listCustomResources("extensions.kuadrant.io", "v1alpha1", "apiproducts");
608
+ const product = (products.items || []).find(
609
+ (p) => p.metadata.name.includes(spec.apiName) || p.spec?.displayName?.toLowerCase().includes(spec.apiName.toLowerCase())
610
+ );
611
+ if (product) {
612
+ const plan = product.spec?.plans?.find((p) => p.tier === spec.planTier);
613
+ if (plan) {
614
+ planLimits = plan.limits;
615
+ }
616
+ }
617
+ } catch (e) {
618
+ console.warn("could not fetch apiproduct for plan limits:", e);
619
+ }
620
+ if (!planLimits) {
621
+ try {
622
+ const policy = await k8sClient$1.getCustomResource(
623
+ "extensions.kuadrant.io",
624
+ "v1alpha1",
625
+ spec.apiNamespace,
626
+ "planpolicies",
627
+ `${spec.apiName}-plan`
628
+ );
629
+ const plan = policy.spec?.plans?.find((p) => p.tier === spec.planTier);
630
+ if (plan) {
631
+ planLimits = plan.limits;
632
+ }
633
+ } catch (e) {
634
+ console.warn("could not fetch planpolicy for plan limits:", e);
635
+ }
636
+ }
637
+ let apiHostname = `${spec.apiName}.apps.example.com`;
638
+ try {
639
+ const httproute = await k8sClient$1.getCustomResource(
640
+ "gateway.networking.k8s.io",
641
+ "v1",
642
+ spec.apiNamespace,
643
+ "httproutes",
644
+ spec.apiName
645
+ );
646
+ if (httproute.spec?.hostnames && httproute.spec.hostnames.length > 0) {
647
+ apiHostname = httproute.spec.hostnames[0];
648
+ }
649
+ } catch (error) {
650
+ console.warn("could not fetch httproute for hostname, using default:", error);
651
+ }
652
+ const status = {
653
+ phase: "Approved",
654
+ reviewedBy,
655
+ reviewedAt: (/* @__PURE__ */ new Date()).toISOString(),
656
+ reason: comment || "approved",
657
+ apiKey,
658
+ apiHostname,
659
+ apiBasePath: "/api/v1",
660
+ apiDescription: `${spec.apiName} api`,
661
+ planLimits
662
+ };
663
+ await k8sClient$1.patchCustomResourceStatus(
664
+ "extensions.kuadrant.io",
665
+ "v1alpha1",
666
+ namespace,
667
+ "apikeyrequests",
668
+ name,
669
+ status
670
+ );
671
+ res.json({ secretName });
672
+ } catch (error) {
673
+ console.error("error approving api key request:", error);
674
+ if (error instanceof errors.NotAllowedError) {
675
+ res.status(403).json({ error: error.message });
676
+ } else {
677
+ res.status(500).json({ error: "failed to approve api key request" });
678
+ }
679
+ }
680
+ });
681
+ router.post("/requests/:namespace/:name/reject", async (req, res) => {
682
+ const parsed = approveRejectSchema.safeParse(req.body);
683
+ if (!parsed.success) {
684
+ throw new errors.InputError(parsed.error.toString());
685
+ }
686
+ try {
687
+ const { userId, isApiOwner } = await getUserIdentity(req, httpAuth, userInfo);
688
+ let canReject = isApiOwner;
689
+ if (!canReject) {
690
+ try {
691
+ const credentials = await httpAuth.credentials(req, { allow: ["none"] });
692
+ if (credentials) {
693
+ const decision = await permissions$1.authorize(
694
+ [{ permission: permissions.kuadrantApiKeyRequestUpdatePermission }],
695
+ { credentials }
696
+ );
697
+ canReject = decision[0].result === pluginPermissionCommon.AuthorizeResult.ALLOW;
698
+ }
699
+ } catch (error) {
700
+ console.warn("permission check failed, using group-based authorization:", error);
701
+ }
702
+ }
703
+ if (!canReject) {
704
+ throw new errors.NotAllowedError("you do not have permission to reject api key requests");
705
+ }
706
+ const { namespace, name } = req.params;
707
+ const { comment } = parsed.data;
708
+ const reviewedBy = `user:default/${userId}`;
709
+ const status = {
710
+ phase: "Rejected",
711
+ reviewedBy,
712
+ reviewedAt: (/* @__PURE__ */ new Date()).toISOString(),
713
+ reason: comment || "rejected"
714
+ };
715
+ await k8sClient$1.patchCustomResourceStatus(
716
+ "extensions.kuadrant.io",
717
+ "v1alpha1",
718
+ namespace,
719
+ "apikeyrequests",
720
+ name,
721
+ status
722
+ );
723
+ res.status(204).send();
724
+ } catch (error) {
725
+ console.error("error rejecting api key request:", error);
726
+ if (error instanceof errors.NotAllowedError) {
727
+ res.status(403).json({ error: error.message });
728
+ } else {
729
+ res.status(500).json({ error: "failed to reject api key request" });
730
+ }
731
+ }
732
+ });
733
+ router.delete("/requests/:namespace/:name", async (req, res) => {
734
+ try {
735
+ const { userId, isPlatformEngineer, isApiOwner } = await getUserIdentity(req, httpAuth, userInfo);
736
+ const { namespace, name } = req.params;
737
+ const request = await k8sClient$1.getCustomResource(
738
+ "extensions.kuadrant.io",
739
+ "v1alpha1",
740
+ namespace,
741
+ "apikeyrequests",
742
+ name
743
+ );
744
+ const requestUserId = request.spec?.requestedBy?.userId;
745
+ const canDeleteAll = isPlatformEngineer || isApiOwner;
746
+ if (!canDeleteAll && requestUserId !== userId) {
747
+ throw new errors.NotAllowedError("you can only delete your own api key requests");
748
+ }
749
+ if (request.status?.phase === "Approved") {
750
+ try {
751
+ const apiNamespace = request.spec?.apiNamespace;
752
+ const apiName = request.spec?.apiName;
753
+ const planTier = request.spec?.planTier;
754
+ const secrets = await k8sClient$1.listSecrets(apiNamespace);
755
+ const matchingSecret = secrets.items?.find((s) => {
756
+ const annotations = s.metadata?.annotations || {};
757
+ return annotations["secret.kuadrant.io/user-id"] === requestUserId && annotations["secret.kuadrant.io/plan-id"] === planTier && s.metadata?.labels?.app === apiName;
758
+ });
759
+ if (matchingSecret) {
760
+ await k8sClient$1.deleteSecret(apiNamespace, matchingSecret.metadata.name);
761
+ }
762
+ } catch (error) {
763
+ console.warn("failed to delete associated secret:", error);
764
+ }
765
+ }
766
+ await k8sClient$1.deleteCustomResource(
767
+ "extensions.kuadrant.io",
768
+ "v1alpha1",
769
+ namespace,
770
+ "apikeyrequests",
771
+ name
772
+ );
773
+ res.status(204).send();
774
+ } catch (error) {
775
+ console.error("error deleting api key request:", error);
776
+ if (error instanceof errors.NotAllowedError) {
777
+ res.status(403).json({ error: error.message });
778
+ } else {
779
+ res.status(500).json({ error: "failed to delete api key request" });
780
+ }
781
+ }
782
+ });
783
+ router.patch("/requests/:namespace/:name", async (req, res) => {
784
+ try {
785
+ const credentials = await httpAuth.credentials(req);
786
+ const decision = await permissions$1.authorize(
787
+ [{ permission: permissions.kuadrantApiKeyRequestUpdatePermission }],
788
+ { credentials }
789
+ );
790
+ if (decision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
791
+ throw new errors.NotAllowedError("unauthorised");
792
+ }
793
+ const { namespace, name } = req.params;
794
+ const patch = req.body;
795
+ const updated = await k8sClient$1.patchCustomResource(
796
+ "extensions.kuadrant.io",
797
+ "v1alpha1",
798
+ namespace,
799
+ "apikeyrequests",
800
+ name,
801
+ patch
802
+ );
803
+ res.json(updated);
804
+ } catch (error) {
805
+ console.error("error updating api key request:", error);
806
+ if (error instanceof errors.NotAllowedError) {
807
+ res.status(403).json({ error: error.message });
808
+ } else {
809
+ res.status(500).json({ error: "failed to update api key request" });
810
+ }
811
+ }
812
+ });
813
+ router.use(pluginPermissionNode.createPermissionIntegrationRouter({
814
+ permissions: permissions.kuadrantPermissions
815
+ }));
816
+ return router;
817
+ }
818
+
819
+ exports.createRouter = createRouter;
820
+ //# sourceMappingURL=router.cjs.js.map