@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.
- package/README.md +28 -0
- package/alpha/module.cjs.js +31 -0
- package/alpha/package.json +11 -0
- package/dist/alpha.cjs.js +31 -0
- package/dist/alpha.cjs.js.map +1 -0
- package/dist/index.cjs.js +35 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/k8s-client.cjs.js +269 -0
- package/dist/k8s-client.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 +124 -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 +131 -0
- package/dist/providers/APIProductEntityProvider.cjs.js.map +1 -0
- package/dist/rbac.cjs.js +27 -0
- package/dist/rbac.cjs.js.map +1 -0
- package/dist/router.cjs.js +820 -0
- package/dist/router.cjs.js.map +1 -0
- package/package.json +78 -0
- package/rbac/index.ts +1 -0
- package/rbac/package.json +11 -0
|
@@ -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
|