@pstefans/kuadrant-backstage-plugin-backend-dynamic 0.0.2-dev
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/README.md +28 -0
- package/dist/configSchema.json +1 -0
- package/dist/index.cjs.js +47 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.ts +67 -0
- package/dist/k8s-client.cjs.js +277 -0
- package/dist/k8s-client.cjs.js.map +1 -0
- package/dist/module.cjs.js +37 -0
- package/dist/module.cjs.js.map +1 -0
- package/dist/permissions-router.cjs.js +13 -0
- package/dist/permissions-router.cjs.js.map +1 -0
- package/dist/permissions.cjs.js +136 -0
- package/dist/permissions.cjs.js.map +1 -0
- package/dist/plugin.cjs.js +38 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/providers/APIProductEntityProvider.cjs.js +139 -0
- package/dist/providers/APIProductEntityProvider.cjs.js.map +1 -0
- package/dist/rbac-module.cjs.js +27 -0
- package/dist/rbac-module.cjs.js.map +1 -0
- package/dist/router.cjs.js +1151 -0
- package/dist/router.cjs.js.map +1 -0
- package/package.json +75 -0
|
@@ -0,0 +1,1151 @@
|
|
|
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 module$1 = require('./module.cjs.js');
|
|
13
|
+
var permissions = require('./permissions.cjs.js');
|
|
14
|
+
|
|
15
|
+
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
|
|
16
|
+
|
|
17
|
+
var express__default = /*#__PURE__*/_interopDefaultCompat(express);
|
|
18
|
+
var Router__default = /*#__PURE__*/_interopDefaultCompat(Router);
|
|
19
|
+
var cors__default = /*#__PURE__*/_interopDefaultCompat(cors);
|
|
20
|
+
|
|
21
|
+
const secretKey = "api_key";
|
|
22
|
+
function extractNameFromEntityRef(entityRef) {
|
|
23
|
+
const parts = entityRef.split("/");
|
|
24
|
+
return parts[parts.length - 1];
|
|
25
|
+
}
|
|
26
|
+
async function getUserIdentity(req, httpAuth, userInfo) {
|
|
27
|
+
const credentials = await httpAuth.credentials(req);
|
|
28
|
+
if (!credentials || !credentials.principal) {
|
|
29
|
+
throw new errors.NotAllowedError("authentication required");
|
|
30
|
+
}
|
|
31
|
+
const info = await userInfo.getUserInfo(credentials);
|
|
32
|
+
const groups = info.ownershipEntityRefs || [];
|
|
33
|
+
console.log(`user identity resolved: userEntityRef=${info.userEntityRef}, groups=${groups.join(",")}`);
|
|
34
|
+
return {
|
|
35
|
+
userEntityRef: info.userEntityRef,
|
|
36
|
+
groups
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
async function createRouter({
|
|
40
|
+
httpAuth,
|
|
41
|
+
userInfo,
|
|
42
|
+
config,
|
|
43
|
+
permissions: permissions$1
|
|
44
|
+
}) {
|
|
45
|
+
const router = Router__default.default();
|
|
46
|
+
router.use(cors__default.default({
|
|
47
|
+
origin: "http://localhost:3000",
|
|
48
|
+
credentials: true
|
|
49
|
+
}));
|
|
50
|
+
router.use(express__default.default.json());
|
|
51
|
+
const k8sClient$1 = new k8sClient.KuadrantK8sClient(config);
|
|
52
|
+
router.get("/apiproducts", async (req, res) => {
|
|
53
|
+
try {
|
|
54
|
+
const credentials = await httpAuth.credentials(req);
|
|
55
|
+
const listDecision = await permissions$1.authorize(
|
|
56
|
+
[{ permission: permissions.kuadrantApiProductListPermission }],
|
|
57
|
+
{ credentials }
|
|
58
|
+
);
|
|
59
|
+
if (listDecision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
|
|
60
|
+
throw new errors.NotAllowedError("unauthorised");
|
|
61
|
+
}
|
|
62
|
+
const { userEntityRef } = await getUserIdentity(req, httpAuth, userInfo);
|
|
63
|
+
const data = await k8sClient$1.listCustomResources("devportal.kuadrant.io", "v1alpha1", "apiproducts");
|
|
64
|
+
const readAllDecision = await permissions$1.authorize(
|
|
65
|
+
[{ permission: permissions.kuadrantApiProductReadAllPermission }],
|
|
66
|
+
{ credentials }
|
|
67
|
+
);
|
|
68
|
+
if (readAllDecision[0].result === pluginPermissionCommon.AuthorizeResult.ALLOW) {
|
|
69
|
+
res.json(data);
|
|
70
|
+
} else {
|
|
71
|
+
const readOwnDecision = await permissions$1.authorize(
|
|
72
|
+
[{ permission: permissions.kuadrantApiProductReadOwnPermission }],
|
|
73
|
+
{ credentials }
|
|
74
|
+
);
|
|
75
|
+
if (readOwnDecision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
|
|
76
|
+
throw new errors.NotAllowedError("unauthorised");
|
|
77
|
+
}
|
|
78
|
+
const ownedItems = (data.items || []).filter((item) => {
|
|
79
|
+
const owner = item.metadata?.annotations?.["backstage.io/owner"];
|
|
80
|
+
return owner === userEntityRef;
|
|
81
|
+
});
|
|
82
|
+
res.json({ ...data, items: ownedItems });
|
|
83
|
+
}
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.error("error fetching apiproducts:", error);
|
|
86
|
+
if (error instanceof errors.NotAllowedError) {
|
|
87
|
+
res.status(403).json({ error: error.message });
|
|
88
|
+
} else {
|
|
89
|
+
res.status(500).json({ error: "failed to fetch apiproducts" });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
router.get("/apiproducts/:namespace/:name", async (req, res) => {
|
|
94
|
+
try {
|
|
95
|
+
const credentials = await httpAuth.credentials(req);
|
|
96
|
+
const { namespace, name } = req.params;
|
|
97
|
+
const readAllDecision = await permissions$1.authorize(
|
|
98
|
+
[{ permission: permissions.kuadrantApiProductReadAllPermission }],
|
|
99
|
+
{ credentials }
|
|
100
|
+
);
|
|
101
|
+
if (readAllDecision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
|
|
102
|
+
const readOwnDecision = await permissions$1.authorize(
|
|
103
|
+
[{ permission: permissions.kuadrantApiProductReadOwnPermission }],
|
|
104
|
+
{ credentials }
|
|
105
|
+
);
|
|
106
|
+
if (readOwnDecision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
|
|
107
|
+
throw new errors.NotAllowedError("unauthorised");
|
|
108
|
+
}
|
|
109
|
+
const { userEntityRef } = await getUserIdentity(req, httpAuth, userInfo);
|
|
110
|
+
const data = await k8sClient$1.getCustomResource("devportal.kuadrant.io", "v1alpha1", namespace, "apiproducts", name);
|
|
111
|
+
const owner = data.metadata?.annotations?.["backstage.io/owner"];
|
|
112
|
+
if (owner !== userEntityRef) {
|
|
113
|
+
throw new errors.NotAllowedError("you can only read your own api products");
|
|
114
|
+
}
|
|
115
|
+
res.json(data);
|
|
116
|
+
} else {
|
|
117
|
+
const data = await k8sClient$1.getCustomResource("devportal.kuadrant.io", "v1alpha1", namespace, "apiproducts", name);
|
|
118
|
+
res.json(data);
|
|
119
|
+
}
|
|
120
|
+
} catch (error) {
|
|
121
|
+
console.error("error fetching apiproduct:", error);
|
|
122
|
+
if (error instanceof errors.NotAllowedError) {
|
|
123
|
+
res.status(403).json({ error: error.message });
|
|
124
|
+
} else {
|
|
125
|
+
res.status(500).json({ error: "failed to fetch apiproduct" });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
router.post("/apiproducts", async (req, res) => {
|
|
130
|
+
try {
|
|
131
|
+
const credentials = await httpAuth.credentials(req);
|
|
132
|
+
const decision = await permissions$1.authorize(
|
|
133
|
+
[{ permission: permissions.kuadrantApiProductCreatePermission }],
|
|
134
|
+
{ credentials }
|
|
135
|
+
);
|
|
136
|
+
if (decision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
|
|
137
|
+
throw new errors.NotAllowedError("unauthorised");
|
|
138
|
+
}
|
|
139
|
+
const { userEntityRef } = await getUserIdentity(req, httpAuth, userInfo);
|
|
140
|
+
const apiProduct = req.body;
|
|
141
|
+
const targetRef = apiProduct.spec?.targetRef;
|
|
142
|
+
if (!targetRef?.name || !targetRef?.kind || !targetRef?.namespace) {
|
|
143
|
+
throw new errors.InputError("targetRef with name, kind, and namespace is required");
|
|
144
|
+
}
|
|
145
|
+
const namespace = targetRef.namespace;
|
|
146
|
+
apiProduct.metadata.namespace = namespace;
|
|
147
|
+
if (!apiProduct.metadata.annotations) {
|
|
148
|
+
apiProduct.metadata.annotations = {};
|
|
149
|
+
}
|
|
150
|
+
apiProduct.metadata.annotations["backstage.io/owner"] = userEntityRef;
|
|
151
|
+
const created = await k8sClient$1.createCustomResource(
|
|
152
|
+
"devportal.kuadrant.io",
|
|
153
|
+
"v1alpha1",
|
|
154
|
+
namespace,
|
|
155
|
+
"apiproducts",
|
|
156
|
+
apiProduct
|
|
157
|
+
);
|
|
158
|
+
const provider = module$1.getAPIProductEntityProvider();
|
|
159
|
+
if (provider) {
|
|
160
|
+
await provider.refresh();
|
|
161
|
+
}
|
|
162
|
+
res.status(201).json(created);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.error("error creating apiproduct:", error);
|
|
165
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
166
|
+
if (error instanceof errors.NotAllowedError) {
|
|
167
|
+
res.status(403).json({ error: error.message });
|
|
168
|
+
} else if (error instanceof errors.InputError) {
|
|
169
|
+
res.status(400).json({ error: error.message });
|
|
170
|
+
} else {
|
|
171
|
+
res.status(500).json({ error: errorMessage });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
router.delete("/apiproducts/:namespace/:name", async (req, res) => {
|
|
176
|
+
try {
|
|
177
|
+
const credentials = await httpAuth.credentials(req);
|
|
178
|
+
const { namespace, name } = req.params;
|
|
179
|
+
const deleteAllDecision = await permissions$1.authorize(
|
|
180
|
+
[{ permission: permissions.kuadrantApiProductDeleteAllPermission }],
|
|
181
|
+
{ credentials }
|
|
182
|
+
);
|
|
183
|
+
if (deleteAllDecision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
|
|
184
|
+
const deleteOwnDecision = await permissions$1.authorize(
|
|
185
|
+
[{ permission: permissions.kuadrantApiProductDeleteOwnPermission }],
|
|
186
|
+
{ credentials }
|
|
187
|
+
);
|
|
188
|
+
if (deleteOwnDecision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
|
|
189
|
+
throw new errors.NotAllowedError("unauthorised");
|
|
190
|
+
}
|
|
191
|
+
const { userEntityRef } = await getUserIdentity(req, httpAuth, userInfo);
|
|
192
|
+
const existing = await k8sClient$1.getCustomResource("devportal.kuadrant.io", "v1alpha1", namespace, "apiproducts", name);
|
|
193
|
+
const owner = existing.metadata?.annotations?.["backstage.io/owner"];
|
|
194
|
+
if (owner !== userEntityRef) {
|
|
195
|
+
throw new errors.NotAllowedError("you can only delete your own api products");
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
console.log(`cascading delete: finding apikeys for ${namespace}/${name}`);
|
|
199
|
+
let allRequests;
|
|
200
|
+
try {
|
|
201
|
+
allRequests = await k8sClient$1.listCustomResources(
|
|
202
|
+
"devportal.kuadrant.io",
|
|
203
|
+
"v1alpha1",
|
|
204
|
+
"apikeys",
|
|
205
|
+
namespace
|
|
206
|
+
);
|
|
207
|
+
} catch (error) {
|
|
208
|
+
console.warn("failed to list apikeys during cascade delete:", error);
|
|
209
|
+
allRequests = { items: [] };
|
|
210
|
+
}
|
|
211
|
+
const relatedRequests = (allRequests.items || []).filter(
|
|
212
|
+
(req2) => req2.spec?.apiProductRef?.name === name
|
|
213
|
+
);
|
|
214
|
+
console.log(`found ${relatedRequests.length} apikeys to delete`);
|
|
215
|
+
const deletionResults = await Promise.allSettled(
|
|
216
|
+
relatedRequests.map(async (request) => {
|
|
217
|
+
const requestName = request.metadata.name;
|
|
218
|
+
console.log(`deleting apikey: ${namespace}/${requestName}`);
|
|
219
|
+
await k8sClient$1.deleteCustomResource(
|
|
220
|
+
"devportal.kuadrant.io",
|
|
221
|
+
"v1alpha1",
|
|
222
|
+
namespace,
|
|
223
|
+
"apikeys",
|
|
224
|
+
requestName
|
|
225
|
+
);
|
|
226
|
+
})
|
|
227
|
+
);
|
|
228
|
+
const failures = deletionResults.filter((r) => r.status === "rejected");
|
|
229
|
+
if (failures.length > 0) {
|
|
230
|
+
console.warn(
|
|
231
|
+
`${failures.length} apikeys failed to delete:`,
|
|
232
|
+
failures.map((f) => f.reason)
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
await k8sClient$1.deleteCustomResource(
|
|
236
|
+
"devportal.kuadrant.io",
|
|
237
|
+
"v1alpha1",
|
|
238
|
+
namespace,
|
|
239
|
+
"apiproducts",
|
|
240
|
+
name
|
|
241
|
+
);
|
|
242
|
+
const provider = module$1.getAPIProductEntityProvider();
|
|
243
|
+
if (provider) {
|
|
244
|
+
await provider.refresh();
|
|
245
|
+
}
|
|
246
|
+
res.status(204).send();
|
|
247
|
+
} catch (error) {
|
|
248
|
+
console.error("error deleting apiproduct:", error);
|
|
249
|
+
if (error instanceof errors.NotAllowedError) {
|
|
250
|
+
res.status(403).json({ error: error.message });
|
|
251
|
+
} else {
|
|
252
|
+
res.status(500).json({ error: "failed to delete apiproduct" });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
router.get("/httproutes", async (req, res) => {
|
|
257
|
+
try {
|
|
258
|
+
const credentials = await httpAuth.credentials(req);
|
|
259
|
+
const decision = await permissions$1.authorize(
|
|
260
|
+
[{ permission: permissions.kuadrantApiProductListPermission }],
|
|
261
|
+
{ credentials }
|
|
262
|
+
);
|
|
263
|
+
if (decision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
|
|
264
|
+
throw new errors.NotAllowedError("unauthorised");
|
|
265
|
+
}
|
|
266
|
+
const data = await k8sClient$1.listCustomResources("gateway.networking.k8s.io", "v1", "httproutes");
|
|
267
|
+
res.json(data);
|
|
268
|
+
} catch (error) {
|
|
269
|
+
console.error("error fetching httproutes:", error);
|
|
270
|
+
if (error instanceof errors.NotAllowedError) {
|
|
271
|
+
res.status(403).json({ error: error.message });
|
|
272
|
+
} else {
|
|
273
|
+
res.status(500).json({ error: "failed to fetch httproutes" });
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
router.patch("/apiproducts/:namespace/:name", async (req, res) => {
|
|
278
|
+
const patchSchema = zod.z.object({
|
|
279
|
+
spec: zod.z.object({
|
|
280
|
+
displayName: zod.z.string().optional(),
|
|
281
|
+
description: zod.z.string().optional(),
|
|
282
|
+
version: zod.z.string().optional(),
|
|
283
|
+
publishStatus: zod.z.enum(["Draft", "Published"]).optional(),
|
|
284
|
+
approvalMode: zod.z.enum(["automatic", "manual"]).optional(),
|
|
285
|
+
tags: zod.z.array(zod.z.string()).optional(),
|
|
286
|
+
contact: zod.z.object({
|
|
287
|
+
email: zod.z.string().optional(),
|
|
288
|
+
team: zod.z.string().optional(),
|
|
289
|
+
slack: zod.z.string().optional()
|
|
290
|
+
}).partial().optional(),
|
|
291
|
+
documentation: zod.z.object({
|
|
292
|
+
docsURL: zod.z.string().optional(),
|
|
293
|
+
openAPISpec: zod.z.string().optional()
|
|
294
|
+
}).partial().optional()
|
|
295
|
+
}).partial()
|
|
296
|
+
});
|
|
297
|
+
const parsed = patchSchema.safeParse(req.body);
|
|
298
|
+
if (!parsed.success) {
|
|
299
|
+
return res.status(400).json({ error: "invalid patch: " + parsed.error.toString() });
|
|
300
|
+
}
|
|
301
|
+
try {
|
|
302
|
+
const credentials = await httpAuth.credentials(req);
|
|
303
|
+
if (!credentials || !credentials.principal) {
|
|
304
|
+
throw new errors.NotAllowedError("authentication required");
|
|
305
|
+
}
|
|
306
|
+
const { namespace, name } = req.params;
|
|
307
|
+
const updateAllDecision = await permissions$1.authorize(
|
|
308
|
+
[{ permission: permissions.kuadrantApiProductUpdateAllPermission }],
|
|
309
|
+
{ credentials }
|
|
310
|
+
);
|
|
311
|
+
if (updateAllDecision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
|
|
312
|
+
const updateOwnDecision = await permissions$1.authorize(
|
|
313
|
+
[{ permission: permissions.kuadrantApiProductUpdateOwnPermission }],
|
|
314
|
+
{ credentials }
|
|
315
|
+
);
|
|
316
|
+
if (updateOwnDecision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
|
|
317
|
+
throw new errors.NotAllowedError("unauthorised");
|
|
318
|
+
}
|
|
319
|
+
const { userEntityRef } = await getUserIdentity(req, httpAuth, userInfo);
|
|
320
|
+
const existing = await k8sClient$1.getCustomResource("devportal.kuadrant.io", "v1alpha1", namespace, "apiproducts", name);
|
|
321
|
+
const owner = existing.metadata?.annotations?.["backstage.io/owner"];
|
|
322
|
+
if (owner !== userEntityRef) {
|
|
323
|
+
throw new errors.NotAllowedError("you can only update your own api products");
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (req.body.metadata?.annotations) {
|
|
327
|
+
delete req.body.metadata.annotations["backstage.io/owner"];
|
|
328
|
+
}
|
|
329
|
+
const updated = await k8sClient$1.patchCustomResource(
|
|
330
|
+
"devportal.kuadrant.io",
|
|
331
|
+
"v1alpha1",
|
|
332
|
+
namespace,
|
|
333
|
+
"apiproducts",
|
|
334
|
+
name,
|
|
335
|
+
parsed.data
|
|
336
|
+
);
|
|
337
|
+
const provider = module$1.getAPIProductEntityProvider();
|
|
338
|
+
if (provider) {
|
|
339
|
+
await provider.refresh();
|
|
340
|
+
}
|
|
341
|
+
return res.json(updated);
|
|
342
|
+
} catch (error) {
|
|
343
|
+
console.error("error updating apiproduct:", error);
|
|
344
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
345
|
+
if (error instanceof errors.NotAllowedError) {
|
|
346
|
+
return res.status(403).json({ error: error.message });
|
|
347
|
+
} else if (error instanceof errors.InputError) {
|
|
348
|
+
return res.status(400).json({ error: error.message });
|
|
349
|
+
} else {
|
|
350
|
+
return res.status(500).json({ error: errorMessage });
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
router.get("/planpolicies", async (req, res) => {
|
|
355
|
+
try {
|
|
356
|
+
const credentials = await httpAuth.credentials(req);
|
|
357
|
+
const decision = await permissions$1.authorize(
|
|
358
|
+
[{ permission: permissions.kuadrantPlanPolicyListPermission }],
|
|
359
|
+
{ credentials }
|
|
360
|
+
);
|
|
361
|
+
if (decision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
|
|
362
|
+
throw new errors.NotAllowedError("unauthorised");
|
|
363
|
+
}
|
|
364
|
+
const data = await k8sClient$1.listCustomResources("extensions.kuadrant.io", "v1alpha1", "planpolicies");
|
|
365
|
+
const filtered = {
|
|
366
|
+
items: (data.items || []).map((policy) => ({
|
|
367
|
+
metadata: {
|
|
368
|
+
name: policy.metadata.name,
|
|
369
|
+
namespace: policy.metadata.namespace
|
|
370
|
+
},
|
|
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
|
+
}))
|
|
383
|
+
}))
|
|
384
|
+
};
|
|
385
|
+
res.json(filtered);
|
|
386
|
+
} catch (error) {
|
|
387
|
+
console.error("error fetching planpolicies:", error);
|
|
388
|
+
if (error instanceof errors.NotAllowedError) {
|
|
389
|
+
res.status(403).json({ error: error.message });
|
|
390
|
+
} else {
|
|
391
|
+
res.status(500).json({ error: "failed to fetch planpolicies" });
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
router.get("/planpolicies/:namespace/:name", async (req, res) => {
|
|
396
|
+
try {
|
|
397
|
+
const credentials = await httpAuth.credentials(req);
|
|
398
|
+
const decision = await permissions$1.authorize(
|
|
399
|
+
[{ permission: permissions.kuadrantPlanPolicyReadPermission }],
|
|
400
|
+
{ credentials }
|
|
401
|
+
);
|
|
402
|
+
if (decision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
|
|
403
|
+
throw new errors.NotAllowedError("unauthorised");
|
|
404
|
+
}
|
|
405
|
+
const { namespace, name } = req.params;
|
|
406
|
+
const data = await k8sClient$1.getCustomResource("extensions.kuadrant.io", "v1alpha1", namespace, "planpolicies", name);
|
|
407
|
+
res.json(data);
|
|
408
|
+
} catch (error) {
|
|
409
|
+
console.error("error fetching planpolicy:", error);
|
|
410
|
+
if (error instanceof errors.NotAllowedError) {
|
|
411
|
+
res.status(403).json({ error: error.message });
|
|
412
|
+
} else {
|
|
413
|
+
res.status(500).json({ error: "failed to fetch planpolicy" });
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
const requestSchema = zod.z.object({
|
|
418
|
+
apiProductName: zod.z.string(),
|
|
419
|
+
// name of the APIProduct
|
|
420
|
+
namespace: zod.z.string(),
|
|
421
|
+
// namespace where both APIProduct and APIKey live
|
|
422
|
+
planTier: zod.z.string(),
|
|
423
|
+
useCase: zod.z.string().optional(),
|
|
424
|
+
userEmail: zod.z.string().optional()
|
|
425
|
+
});
|
|
426
|
+
router.post("/requests", async (req, res) => {
|
|
427
|
+
const parsed = requestSchema.safeParse(req.body);
|
|
428
|
+
if (!parsed.success) {
|
|
429
|
+
throw new errors.InputError(parsed.error.toString());
|
|
430
|
+
}
|
|
431
|
+
try {
|
|
432
|
+
const credentials = await httpAuth.credentials(req);
|
|
433
|
+
const { apiProductName, namespace, planTier, useCase, userEmail } = parsed.data;
|
|
434
|
+
const { userEntityRef } = await getUserIdentity(req, httpAuth, userInfo);
|
|
435
|
+
const resourceRef = `apiproduct:${namespace}/${apiProductName}`;
|
|
436
|
+
const decision = await permissions$1.authorize(
|
|
437
|
+
[{
|
|
438
|
+
permission: permissions.kuadrantApiKeyCreatePermission,
|
|
439
|
+
resourceRef
|
|
440
|
+
}],
|
|
441
|
+
{ credentials }
|
|
442
|
+
);
|
|
443
|
+
if (decision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
|
|
444
|
+
throw new errors.NotAllowedError(`not authorised to request access to ${apiProductName}`);
|
|
445
|
+
}
|
|
446
|
+
const randomSuffix = crypto.randomBytes(4).toString("hex");
|
|
447
|
+
const userName = extractNameFromEntityRef(userEntityRef);
|
|
448
|
+
const requestName = `${userName}-${apiProductName}-${randomSuffix}`.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
449
|
+
const requestedBy = { userId: userEntityRef };
|
|
450
|
+
if (userEmail) {
|
|
451
|
+
requestedBy.email = userEmail;
|
|
452
|
+
}
|
|
453
|
+
const request = {
|
|
454
|
+
apiVersion: "devportal.kuadrant.io/v1alpha1",
|
|
455
|
+
kind: "APIKey",
|
|
456
|
+
metadata: {
|
|
457
|
+
name: requestName,
|
|
458
|
+
namespace
|
|
459
|
+
},
|
|
460
|
+
spec: {
|
|
461
|
+
apiProductRef: {
|
|
462
|
+
name: apiProductName
|
|
463
|
+
},
|
|
464
|
+
planTier,
|
|
465
|
+
useCase: useCase || "",
|
|
466
|
+
requestedBy
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
const created = await k8sClient$1.createCustomResource(
|
|
470
|
+
"devportal.kuadrant.io",
|
|
471
|
+
"v1alpha1",
|
|
472
|
+
namespace,
|
|
473
|
+
"apikeys",
|
|
474
|
+
request
|
|
475
|
+
);
|
|
476
|
+
res.status(201).json(created);
|
|
477
|
+
} catch (error) {
|
|
478
|
+
console.error("error creating api key request:", error);
|
|
479
|
+
if (error instanceof errors.NotAllowedError) {
|
|
480
|
+
res.status(403).json({ error: error.message });
|
|
481
|
+
} else {
|
|
482
|
+
res.status(500).json({ error: "failed to create api key request" });
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
router.get("/requests", async (req, res) => {
|
|
487
|
+
try {
|
|
488
|
+
const credentials = await httpAuth.credentials(req);
|
|
489
|
+
const readAllDecision = await permissions$1.authorize(
|
|
490
|
+
[{ permission: permissions.kuadrantApiKeyReadAllPermission }],
|
|
491
|
+
{ credentials }
|
|
492
|
+
);
|
|
493
|
+
const canReadAll = readAllDecision[0].result === pluginPermissionCommon.AuthorizeResult.ALLOW;
|
|
494
|
+
if (!canReadAll) {
|
|
495
|
+
const readOwnDecision = await permissions$1.authorize(
|
|
496
|
+
[{ permission: permissions.kuadrantApiKeyReadOwnPermission }],
|
|
497
|
+
{ credentials }
|
|
498
|
+
);
|
|
499
|
+
if (readOwnDecision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
|
|
500
|
+
throw new errors.NotAllowedError("unauthorised");
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
const status = req.query.status;
|
|
504
|
+
const namespace = req.query.namespace;
|
|
505
|
+
let data;
|
|
506
|
+
if (namespace) {
|
|
507
|
+
data = await k8sClient$1.listCustomResources("devportal.kuadrant.io", "v1alpha1", "apikeys", namespace);
|
|
508
|
+
} else {
|
|
509
|
+
data = await k8sClient$1.listCustomResources("devportal.kuadrant.io", "v1alpha1", "apikeys");
|
|
510
|
+
}
|
|
511
|
+
let filteredItems = data.items || [];
|
|
512
|
+
if (!canReadAll) {
|
|
513
|
+
const { userEntityRef } = await getUserIdentity(req, httpAuth, userInfo);
|
|
514
|
+
const apiproducts = await k8sClient$1.listCustomResources("devportal.kuadrant.io", "v1alpha1", "apiproducts");
|
|
515
|
+
const ownedApiProducts = (apiproducts.items || []).filter((product) => {
|
|
516
|
+
const owner = product.metadata?.annotations?.["backstage.io/owner"];
|
|
517
|
+
return owner === userEntityRef;
|
|
518
|
+
}).map((product) => product.metadata.name);
|
|
519
|
+
filteredItems = filteredItems.filter(
|
|
520
|
+
(req2) => ownedApiProducts.includes(req2.spec?.apiProductRef?.name)
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
if (status) {
|
|
524
|
+
filteredItems = filteredItems.filter((req2) => {
|
|
525
|
+
const phase = req2.status?.phase || "Pending";
|
|
526
|
+
return phase === status;
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
res.json({ items: filteredItems });
|
|
530
|
+
} catch (error) {
|
|
531
|
+
console.error("error fetching api key requests:", error);
|
|
532
|
+
if (error instanceof errors.NotAllowedError) {
|
|
533
|
+
res.status(403).json({ error: error.message });
|
|
534
|
+
} else {
|
|
535
|
+
res.status(500).json({ error: "failed to fetch api key requests" });
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
router.get("/requests/my", async (req, res) => {
|
|
540
|
+
try {
|
|
541
|
+
const credentials = await httpAuth.credentials(req);
|
|
542
|
+
const decision = await permissions$1.authorize(
|
|
543
|
+
[{ permission: permissions.kuadrantApiKeyReadOwnPermission }],
|
|
544
|
+
{ credentials }
|
|
545
|
+
);
|
|
546
|
+
if (decision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
|
|
547
|
+
throw new errors.NotAllowedError("unauthorised");
|
|
548
|
+
}
|
|
549
|
+
const { userEntityRef } = await getUserIdentity(req, httpAuth, userInfo);
|
|
550
|
+
const namespace = req.query.namespace;
|
|
551
|
+
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");
|
|
556
|
+
}
|
|
557
|
+
const filteredItems = (data.items || []).filter(
|
|
558
|
+
(req2) => req2.spec?.requestedBy?.userId === userEntityRef
|
|
559
|
+
);
|
|
560
|
+
res.json({ items: filteredItems });
|
|
561
|
+
} catch (error) {
|
|
562
|
+
console.error("error fetching user api key requests:", error);
|
|
563
|
+
if (error instanceof errors.NotAllowedError) {
|
|
564
|
+
res.status(403).json({ error: error.message });
|
|
565
|
+
} else {
|
|
566
|
+
res.status(500).json({ error: "failed to fetch user api key requests" });
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
const approveRejectSchema = zod.z.object({
|
|
571
|
+
comment: zod.z.string().optional()
|
|
572
|
+
});
|
|
573
|
+
router.post("/requests/:namespace/:name/approve", async (req, res) => {
|
|
574
|
+
const parsed = approveRejectSchema.safeParse(req.body);
|
|
575
|
+
if (!parsed.success) {
|
|
576
|
+
throw new errors.InputError(parsed.error.toString());
|
|
577
|
+
}
|
|
578
|
+
try {
|
|
579
|
+
const credentials = await httpAuth.credentials(req);
|
|
580
|
+
const { userEntityRef } = await getUserIdentity(req, httpAuth, userInfo);
|
|
581
|
+
const { namespace, name } = req.params;
|
|
582
|
+
const reviewedBy = userEntityRef;
|
|
583
|
+
const request = await k8sClient$1.getCustomResource(
|
|
584
|
+
"devportal.kuadrant.io",
|
|
585
|
+
"v1alpha1",
|
|
586
|
+
namespace,
|
|
587
|
+
"apikeys",
|
|
588
|
+
name
|
|
589
|
+
);
|
|
590
|
+
const spec = request.spec;
|
|
591
|
+
const apiProductName = spec.apiProductRef?.name;
|
|
592
|
+
if (!apiProductName) {
|
|
593
|
+
throw new errors.InputError("apiProductRef.name is required in APIKey spec");
|
|
594
|
+
}
|
|
595
|
+
const apiProduct = await k8sClient$1.getCustomResource(
|
|
596
|
+
"devportal.kuadrant.io",
|
|
597
|
+
"v1alpha1",
|
|
598
|
+
namespace,
|
|
599
|
+
"apiproducts",
|
|
600
|
+
apiProductName
|
|
601
|
+
);
|
|
602
|
+
const owner = apiProduct.metadata?.annotations?.["backstage.io/owner"];
|
|
603
|
+
const updateAllDecision = await permissions$1.authorize(
|
|
604
|
+
[{ permission: permissions.kuadrantApiKeyUpdateAllPermission }],
|
|
605
|
+
{ credentials }
|
|
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");
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
const status = {
|
|
620
|
+
phase: "Approved",
|
|
621
|
+
reviewedBy,
|
|
622
|
+
reviewedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
623
|
+
};
|
|
624
|
+
await k8sClient$1.patchCustomResourceStatus(
|
|
625
|
+
"devportal.kuadrant.io",
|
|
626
|
+
"v1alpha1",
|
|
627
|
+
namespace,
|
|
628
|
+
"apikeys",
|
|
629
|
+
name,
|
|
630
|
+
status
|
|
631
|
+
);
|
|
632
|
+
res.json({ success: true });
|
|
633
|
+
} catch (error) {
|
|
634
|
+
console.error("error approving api key request:", error);
|
|
635
|
+
if (error instanceof errors.NotAllowedError) {
|
|
636
|
+
res.status(403).json({ error: error.message });
|
|
637
|
+
} else {
|
|
638
|
+
res.status(500).json({ error: "failed to approve api key request" });
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
router.post("/requests/:namespace/:name/reject", async (req, res) => {
|
|
643
|
+
const parsed = approveRejectSchema.safeParse(req.body);
|
|
644
|
+
if (!parsed.success) {
|
|
645
|
+
throw new errors.InputError(parsed.error.toString());
|
|
646
|
+
}
|
|
647
|
+
try {
|
|
648
|
+
const credentials = await httpAuth.credentials(req);
|
|
649
|
+
const { userEntityRef } = await getUserIdentity(req, httpAuth, userInfo);
|
|
650
|
+
const { namespace, name } = req.params;
|
|
651
|
+
const reviewedBy = userEntityRef;
|
|
652
|
+
const request = await k8sClient$1.getCustomResource(
|
|
653
|
+
"devportal.kuadrant.io",
|
|
654
|
+
"v1alpha1",
|
|
655
|
+
namespace,
|
|
656
|
+
"apikeys",
|
|
657
|
+
name
|
|
658
|
+
);
|
|
659
|
+
const spec = request.spec;
|
|
660
|
+
const apiProductName = spec.apiProductRef?.name;
|
|
661
|
+
if (!apiProductName) {
|
|
662
|
+
throw new errors.InputError("apiProductRef.name is required in APIKey spec");
|
|
663
|
+
}
|
|
664
|
+
const apiProduct = await k8sClient$1.getCustomResource(
|
|
665
|
+
"devportal.kuadrant.io",
|
|
666
|
+
"v1alpha1",
|
|
667
|
+
namespace,
|
|
668
|
+
"apiproducts",
|
|
669
|
+
apiProductName
|
|
670
|
+
);
|
|
671
|
+
const owner = apiProduct.metadata?.annotations?.["backstage.io/owner"];
|
|
672
|
+
const updateAllDecision = await permissions$1.authorize(
|
|
673
|
+
[{ permission: permissions.kuadrantApiKeyUpdateAllPermission }],
|
|
674
|
+
{ credentials }
|
|
675
|
+
);
|
|
676
|
+
if (updateAllDecision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
|
|
677
|
+
const updateOwnDecision = await permissions$1.authorize(
|
|
678
|
+
[{ permission: permissions.kuadrantApiKeyUpdateOwnPermission }],
|
|
679
|
+
{ credentials }
|
|
680
|
+
);
|
|
681
|
+
if (updateOwnDecision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
|
|
682
|
+
throw new errors.NotAllowedError("unauthorised");
|
|
683
|
+
}
|
|
684
|
+
if (owner !== userEntityRef) {
|
|
685
|
+
throw new errors.NotAllowedError("you can only reject requests for your own api products");
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
const status = {
|
|
689
|
+
phase: "Rejected",
|
|
690
|
+
reviewedBy,
|
|
691
|
+
reviewedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
692
|
+
};
|
|
693
|
+
await k8sClient$1.patchCustomResourceStatus(
|
|
694
|
+
"devportal.kuadrant.io",
|
|
695
|
+
"v1alpha1",
|
|
696
|
+
namespace,
|
|
697
|
+
"apikeys",
|
|
698
|
+
name,
|
|
699
|
+
status
|
|
700
|
+
);
|
|
701
|
+
res.status(204).send();
|
|
702
|
+
} catch (error) {
|
|
703
|
+
console.error("error rejecting api key request:", error);
|
|
704
|
+
if (error instanceof errors.NotAllowedError) {
|
|
705
|
+
res.status(403).json({ error: error.message });
|
|
706
|
+
} else {
|
|
707
|
+
res.status(500).json({ error: "failed to reject api key request" });
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
});
|
|
711
|
+
const bulkApproveSchema = zod.z.object({
|
|
712
|
+
requests: zod.z.array(zod.z.object({
|
|
713
|
+
namespace: zod.z.string(),
|
|
714
|
+
name: zod.z.string()
|
|
715
|
+
})),
|
|
716
|
+
comment: zod.z.string().optional()
|
|
717
|
+
});
|
|
718
|
+
router.post("/requests/bulk-approve", async (req, res) => {
|
|
719
|
+
const parsed = bulkApproveSchema.safeParse(req.body);
|
|
720
|
+
if (!parsed.success) {
|
|
721
|
+
throw new errors.InputError(parsed.error.toString());
|
|
722
|
+
}
|
|
723
|
+
try {
|
|
724
|
+
const credentials = await httpAuth.credentials(req);
|
|
725
|
+
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;
|
|
741
|
+
const reviewedBy = userEntityRef;
|
|
742
|
+
const results = [];
|
|
743
|
+
for (const reqRef of requests) {
|
|
744
|
+
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
|
+
}
|
|
780
|
+
}
|
|
781
|
+
const status = {
|
|
782
|
+
phase: "Approved",
|
|
783
|
+
reviewedBy,
|
|
784
|
+
reviewedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
785
|
+
};
|
|
786
|
+
await k8sClient$1.patchCustomResourceStatus(
|
|
787
|
+
"devportal.kuadrant.io",
|
|
788
|
+
"v1alpha1",
|
|
789
|
+
reqRef.namespace,
|
|
790
|
+
"apikeys",
|
|
791
|
+
reqRef.name,
|
|
792
|
+
status
|
|
793
|
+
);
|
|
794
|
+
results.push({ namespace: reqRef.namespace, name: reqRef.name, success: true });
|
|
795
|
+
} catch (error) {
|
|
796
|
+
console.error(`error approving request ${reqRef.namespace}/${reqRef.name}:`, error);
|
|
797
|
+
results.push({
|
|
798
|
+
namespace: reqRef.namespace,
|
|
799
|
+
name: reqRef.name,
|
|
800
|
+
success: false,
|
|
801
|
+
error: error instanceof Error ? error.message : "unknown error"
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
res.json({ results });
|
|
806
|
+
} catch (error) {
|
|
807
|
+
console.error("error in bulk approve:", error);
|
|
808
|
+
if (error instanceof errors.NotAllowedError) {
|
|
809
|
+
res.status(403).json({ error: error.message });
|
|
810
|
+
} else {
|
|
811
|
+
res.status(500).json({ error: "failed to bulk approve api key requests" });
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
});
|
|
815
|
+
router.post("/requests/bulk-reject", async (req, res) => {
|
|
816
|
+
const parsed = bulkApproveSchema.safeParse(req.body);
|
|
817
|
+
if (!parsed.success) {
|
|
818
|
+
throw new errors.InputError(parsed.error.toString());
|
|
819
|
+
}
|
|
820
|
+
try {
|
|
821
|
+
const credentials = await httpAuth.credentials(req);
|
|
822
|
+
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;
|
|
838
|
+
const reviewedBy = userEntityRef;
|
|
839
|
+
const results = [];
|
|
840
|
+
for (const reqRef of requests) {
|
|
841
|
+
try {
|
|
842
|
+
const request = await k8sClient$1.getCustomResource(
|
|
843
|
+
"devportal.kuadrant.io",
|
|
844
|
+
"v1alpha1",
|
|
845
|
+
reqRef.namespace,
|
|
846
|
+
"apikeys",
|
|
847
|
+
reqRef.name
|
|
848
|
+
);
|
|
849
|
+
const spec = request.spec;
|
|
850
|
+
const apiProduct = await k8sClient$1.getCustomResource(
|
|
851
|
+
"devportal.kuadrant.io",
|
|
852
|
+
"v1alpha1",
|
|
853
|
+
reqRef.namespace,
|
|
854
|
+
"apiproducts",
|
|
855
|
+
spec.apiProductRef?.name
|
|
856
|
+
);
|
|
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()
|
|
871
|
+
};
|
|
872
|
+
await k8sClient$1.patchCustomResourceStatus(
|
|
873
|
+
"devportal.kuadrant.io",
|
|
874
|
+
"v1alpha1",
|
|
875
|
+
reqRef.namespace,
|
|
876
|
+
"apikeys",
|
|
877
|
+
reqRef.name,
|
|
878
|
+
status
|
|
879
|
+
);
|
|
880
|
+
results.push({ namespace: reqRef.namespace, name: reqRef.name, success: true });
|
|
881
|
+
} catch (error) {
|
|
882
|
+
console.error(`error rejecting request ${reqRef.namespace}/${reqRef.name}:`, error);
|
|
883
|
+
results.push({
|
|
884
|
+
namespace: reqRef.namespace,
|
|
885
|
+
name: reqRef.name,
|
|
886
|
+
success: false,
|
|
887
|
+
error: error instanceof Error ? error.message : "unknown error"
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
res.json({ results });
|
|
892
|
+
} catch (error) {
|
|
893
|
+
console.error("error in bulk reject:", error);
|
|
894
|
+
if (error instanceof errors.NotAllowedError) {
|
|
895
|
+
res.status(403).json({ error: error.message });
|
|
896
|
+
} else {
|
|
897
|
+
res.status(500).json({ error: "failed to bulk reject api key requests" });
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
});
|
|
901
|
+
router.delete("/requests/:namespace/:name", async (req, res) => {
|
|
902
|
+
try {
|
|
903
|
+
const credentials = await httpAuth.credentials(req);
|
|
904
|
+
const { userEntityRef } = await getUserIdentity(req, httpAuth, userInfo);
|
|
905
|
+
const { namespace, name } = req.params;
|
|
906
|
+
const request = await k8sClient$1.getCustomResource(
|
|
907
|
+
"devportal.kuadrant.io",
|
|
908
|
+
"v1alpha1",
|
|
909
|
+
namespace,
|
|
910
|
+
"apikeys",
|
|
911
|
+
name
|
|
912
|
+
);
|
|
913
|
+
const requestUserId = request.spec?.requestedBy?.userId;
|
|
914
|
+
const deleteAllDecision = await permissions$1.authorize(
|
|
915
|
+
[{ permission: permissions.kuadrantApiKeyDeleteAllPermission }],
|
|
916
|
+
{ credentials }
|
|
917
|
+
);
|
|
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
|
+
}
|
|
930
|
+
}
|
|
931
|
+
await k8sClient$1.deleteCustomResource(
|
|
932
|
+
"devportal.kuadrant.io",
|
|
933
|
+
"v1alpha1",
|
|
934
|
+
namespace,
|
|
935
|
+
"apikeys",
|
|
936
|
+
name
|
|
937
|
+
);
|
|
938
|
+
res.status(204).send();
|
|
939
|
+
} catch (error) {
|
|
940
|
+
console.error("error deleting api key request:", error);
|
|
941
|
+
if (error instanceof errors.NotAllowedError) {
|
|
942
|
+
res.status(403).json({ error: error.message });
|
|
943
|
+
} else {
|
|
944
|
+
res.status(500).json({ error: "failed to delete api key request" });
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
});
|
|
948
|
+
router.patch("/requests/:namespace/:name", async (req, res) => {
|
|
949
|
+
const patchSchema = zod.z.object({
|
|
950
|
+
spec: zod.z.object({
|
|
951
|
+
useCase: zod.z.string().optional(),
|
|
952
|
+
planTier: zod.z.string().optional()
|
|
953
|
+
}).partial()
|
|
954
|
+
});
|
|
955
|
+
const parsed = patchSchema.safeParse(req.body);
|
|
956
|
+
if (!parsed.success) {
|
|
957
|
+
throw new errors.InputError("invalid patch: " + parsed.error.toString());
|
|
958
|
+
}
|
|
959
|
+
try {
|
|
960
|
+
const credentials = await httpAuth.credentials(req);
|
|
961
|
+
const { userEntityRef } = await getUserIdentity(req, httpAuth, userInfo);
|
|
962
|
+
const { namespace, name } = req.params;
|
|
963
|
+
const existing = await k8sClient$1.getCustomResource(
|
|
964
|
+
"devportal.kuadrant.io",
|
|
965
|
+
"v1alpha1",
|
|
966
|
+
namespace,
|
|
967
|
+
"apikeys",
|
|
968
|
+
name
|
|
969
|
+
);
|
|
970
|
+
const requestUserId = existing.spec?.requestedBy?.userId;
|
|
971
|
+
const currentPhase = existing.status?.phase || "Pending";
|
|
972
|
+
if (currentPhase !== "Pending") {
|
|
973
|
+
throw new errors.NotAllowedError("only pending requests can be edited");
|
|
974
|
+
}
|
|
975
|
+
const updateAllDecision = await permissions$1.authorize(
|
|
976
|
+
[{ permission: permissions.kuadrantApiKeyUpdateAllPermission }],
|
|
977
|
+
{ credentials }
|
|
978
|
+
);
|
|
979
|
+
if (updateAllDecision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
|
|
980
|
+
const updateOwnDecision = await permissions$1.authorize(
|
|
981
|
+
[{ permission: permissions.kuadrantApiKeyUpdateOwnPermission }],
|
|
982
|
+
{ credentials }
|
|
983
|
+
);
|
|
984
|
+
if (updateOwnDecision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
|
|
985
|
+
throw new errors.NotAllowedError("unauthorised");
|
|
986
|
+
}
|
|
987
|
+
if (requestUserId !== userEntityRef) {
|
|
988
|
+
throw new errors.NotAllowedError("you can only update your own api key requests");
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
const updated = await k8sClient$1.patchCustomResource(
|
|
992
|
+
"devportal.kuadrant.io",
|
|
993
|
+
"v1alpha1",
|
|
994
|
+
namespace,
|
|
995
|
+
"apikeys",
|
|
996
|
+
name,
|
|
997
|
+
parsed.data
|
|
998
|
+
);
|
|
999
|
+
res.json(updated);
|
|
1000
|
+
} catch (error) {
|
|
1001
|
+
console.error("error updating api key request:", error);
|
|
1002
|
+
if (error instanceof errors.NotAllowedError) {
|
|
1003
|
+
res.status(403).json({ error: error.message });
|
|
1004
|
+
} else if (error instanceof errors.InputError) {
|
|
1005
|
+
res.status(400).json({ error: error.message });
|
|
1006
|
+
} else {
|
|
1007
|
+
res.status(500).json({ error: "failed to update api key request" });
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
});
|
|
1011
|
+
router.get("/apikeys/:namespace/:name", async (req, res) => {
|
|
1012
|
+
try {
|
|
1013
|
+
const credentials = await httpAuth.credentials(req);
|
|
1014
|
+
const { userEntityRef } = await getUserIdentity(req, httpAuth, userInfo);
|
|
1015
|
+
const { namespace, name } = req.params;
|
|
1016
|
+
const readAllDecision = await permissions$1.authorize(
|
|
1017
|
+
[{ permission: permissions.kuadrantApiKeyReadAllPermission }],
|
|
1018
|
+
{ credentials }
|
|
1019
|
+
);
|
|
1020
|
+
const canReadAll = readAllDecision[0].result === pluginPermissionCommon.AuthorizeResult.ALLOW;
|
|
1021
|
+
if (!canReadAll) {
|
|
1022
|
+
const readOwnDecision = await permissions$1.authorize(
|
|
1023
|
+
[{ permission: permissions.kuadrantApiKeyReadOwnPermission }],
|
|
1024
|
+
{ credentials }
|
|
1025
|
+
);
|
|
1026
|
+
if (readOwnDecision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
|
|
1027
|
+
throw new errors.NotAllowedError("unauthorised");
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
const apiKey = await k8sClient$1.getCustomResource(
|
|
1031
|
+
"devportal.kuadrant.io",
|
|
1032
|
+
"v1alpha1",
|
|
1033
|
+
namespace,
|
|
1034
|
+
"apikeys",
|
|
1035
|
+
name
|
|
1036
|
+
);
|
|
1037
|
+
if (!apiKey) {
|
|
1038
|
+
res.status(404).json({ error: "API key not found" });
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
if (!canReadAll) {
|
|
1042
|
+
const ownerId = apiKey.spec?.requestedBy?.userId;
|
|
1043
|
+
if (ownerId !== userEntityRef) {
|
|
1044
|
+
throw new errors.NotAllowedError("not authorised to view this API key");
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
res.status(200).json(apiKey);
|
|
1048
|
+
} catch (error) {
|
|
1049
|
+
console.error("failed to get api key:", error);
|
|
1050
|
+
if (error instanceof errors.NotAllowedError) {
|
|
1051
|
+
res.status(403).json({ error: error.message });
|
|
1052
|
+
} else {
|
|
1053
|
+
res.status(500).json({ error: "failed to get api key" });
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
});
|
|
1057
|
+
router.get("/apikeys/:namespace/:name/secret", async (req, res) => {
|
|
1058
|
+
try {
|
|
1059
|
+
const credentials = await httpAuth.credentials(req);
|
|
1060
|
+
const { userEntityRef } = await getUserIdentity(req, httpAuth, userInfo);
|
|
1061
|
+
const { namespace, name } = req.params;
|
|
1062
|
+
const readAllDecision = await permissions$1.authorize(
|
|
1063
|
+
[{ permission: permissions.kuadrantApiKeyReadAllPermission }],
|
|
1064
|
+
{ credentials }
|
|
1065
|
+
);
|
|
1066
|
+
const canReadAll = readAllDecision[0].result === pluginPermissionCommon.AuthorizeResult.ALLOW;
|
|
1067
|
+
if (!canReadAll) {
|
|
1068
|
+
const readOwnDecision = await permissions$1.authorize(
|
|
1069
|
+
[{ permission: permissions.kuadrantApiKeyReadOwnPermission }],
|
|
1070
|
+
{ credentials }
|
|
1071
|
+
);
|
|
1072
|
+
if (readOwnDecision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
|
|
1073
|
+
throw new errors.NotAllowedError("unauthorised");
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
const apiKey = await k8sClient$1.getCustomResource(
|
|
1077
|
+
"devportal.kuadrant.io",
|
|
1078
|
+
"v1alpha1",
|
|
1079
|
+
namespace,
|
|
1080
|
+
"apikeys",
|
|
1081
|
+
name
|
|
1082
|
+
);
|
|
1083
|
+
if (!canReadAll) {
|
|
1084
|
+
const requestUserId = apiKey.spec?.requestedBy?.userId;
|
|
1085
|
+
if (requestUserId !== userEntityRef) {
|
|
1086
|
+
throw new errors.NotAllowedError("you can only read your own api key secrets");
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
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) {
|
|
1096
|
+
res.status(404).json({
|
|
1097
|
+
error: "secret reference not found in apikey status"
|
|
1098
|
+
});
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
const secretName = apiKey.status.secretRef.name;
|
|
1102
|
+
let secret;
|
|
1103
|
+
try {
|
|
1104
|
+
secret = await k8sClient$1.getSecret(namespace, secretName);
|
|
1105
|
+
} catch (error) {
|
|
1106
|
+
console.error("error fetching secret:", error);
|
|
1107
|
+
res.status(404).json({
|
|
1108
|
+
error: "secret not found"
|
|
1109
|
+
});
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
const secretData = secret.data || {};
|
|
1113
|
+
const apiKeyValue = secretData[secretKey];
|
|
1114
|
+
if (!apiKeyValue) {
|
|
1115
|
+
res.status(404).json({
|
|
1116
|
+
error: `secret key '${secretKey}' not found in secret`
|
|
1117
|
+
});
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
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
|
+
res.json({
|
|
1133
|
+
apiKey: decodedApiKey
|
|
1134
|
+
});
|
|
1135
|
+
} catch (error) {
|
|
1136
|
+
console.error("error reading api key secret:", error);
|
|
1137
|
+
if (error instanceof errors.NotAllowedError) {
|
|
1138
|
+
res.status(403).json({ error: error.message });
|
|
1139
|
+
} else {
|
|
1140
|
+
res.status(500).json({ error: "failed to read api key secret" });
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
});
|
|
1144
|
+
router.use(pluginPermissionNode.createPermissionIntegrationRouter({
|
|
1145
|
+
permissions: permissions.kuadrantPermissions
|
|
1146
|
+
}));
|
|
1147
|
+
return router;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
exports.createRouter = createRouter;
|
|
1151
|
+
//# sourceMappingURL=router.cjs.js.map
|