@acarmisc/backstage-plugin-litellm-backend 0.17.7 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -0
- package/config.d.ts +28 -0
- package/dist/bridge.d.ts +4 -6
- package/dist/bridge.js +10 -28
- package/dist/client.d.ts +7 -20
- package/dist/client.js +29 -107
- package/dist/errors.d.ts +2 -1
- package/dist/errors.js +4 -1
- package/dist/index.cjs.js +366 -480
- package/dist/index.cjs.js.map +4 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -3
- package/dist/provisioning.d.ts +2 -0
- package/dist/provisioning.js +11 -20
- package/dist/router.js +56 -34
- package/dist/routes/bridge.js +10 -19
- package/dist/routes/config.js +2 -0
- package/dist/routes/context.d.ts +4 -2
- package/dist/routes/keys.js +8 -21
- package/dist/routes/middleware/withUser.d.ts +3 -2
- package/dist/routes/middleware/withUser.js +1 -1
- package/dist/routes/objectPermissions.js +4 -8
- package/dist/routes/teamUsage.js +2 -3
- package/dist/routes/teams.js +50 -24
- package/dist/services/keyService.d.ts +10 -13
- package/dist/services/keyService.js +12 -47
- package/dist/teamAdmin.d.ts +24 -0
- package/dist/teamAdmin.js +3 -0
- package/dist/types.cjs.js.map +1 -1
- package/dist/types.d.ts +0 -36
- package/package.json +3 -7
package/dist/index.d.ts
CHANGED
|
@@ -2,13 +2,13 @@ export { litellmPlugin } from './plugin';
|
|
|
2
2
|
export { litellmPlugin as default } from './plugin';
|
|
3
3
|
export { createRouter } from './router';
|
|
4
4
|
export * from './types';
|
|
5
|
-
export { LiteLLMClient
|
|
5
|
+
export { LiteLLMClient } from './client';
|
|
6
6
|
export { resolveUserId, resolveUserProfile, toLiteLLMUserId, getOrProvisionUser, provisionUser, readProvisioningDefaults, readRoleConfigs, applyRoleOverrides, resolveUserRole, ProvisioningError, } from './provisioning';
|
|
7
7
|
export type { BackstageUserProfile } from './provisioning';
|
|
8
8
|
export { readTeamAdminConfig, isTeamManagementEnabled, assertTeamAdmin, validateTeamWriteInput, validateTeamPatchInput, } from './teamAdmin';
|
|
9
9
|
export type { TeamAdminConfig, TeamAdminCheck, TeamWriteInput, TeamWriteValidation, TeamPatchValidation } from './teamAdmin';
|
|
10
10
|
export { readTeamBudgetVisibility, redactTeamBudget, redactTeamUsage, teamBudgetStatusFor, } from './teamBudgetVisibility';
|
|
11
11
|
export type { TeamBudgetVisibility, TeamBudgetStatus } from './teamBudgetVisibility';
|
|
12
|
-
export { KeycloakJWTVerifier, newDefaultVerifier, readBridgeConfig, resolveBridgeUserId, getOrProvisionUserFromClaims, bridgeListKeys,
|
|
12
|
+
export { KeycloakJWTVerifier, newDefaultVerifier, readBridgeConfig, resolveBridgeUserId, getOrProvisionUserFromClaims, bridgeListKeys, } from './bridge';
|
|
13
13
|
export type { BridgeClaims, TokenVerifier, BridgeConfig, KeycloakJWTVerifierOptions, } from './bridge';
|
|
14
14
|
export { litellmPermissions, litellmKeyCreatePermission, litellmKeyRevokePermission, litellmKeyManagePermission, litellmKeyResetSpendPermission, litellmKeyUnblockPermission, litellmAuditReadPermission, litellmTeamCreatePermission, litellmTeamManagePermission, litellmTeamMembersManagePermission, litellmTeamKnowledgebaseManagePermission, litellmTeamMcpManagePermission, litellmTeamDeletePermission, } from '@acarmisc/backstage-plugin-litellm-common';
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.litellmTeamDeletePermission = exports.litellmTeamMcpManagePermission = exports.litellmTeamKnowledgebaseManagePermission = exports.litellmTeamMembersManagePermission = exports.litellmTeamManagePermission = exports.litellmTeamCreatePermission = exports.litellmAuditReadPermission = exports.litellmKeyUnblockPermission = exports.litellmKeyResetSpendPermission = exports.litellmKeyManagePermission = exports.litellmKeyRevokePermission = exports.litellmKeyCreatePermission = exports.litellmPermissions = exports.
|
|
17
|
+
exports.litellmTeamDeletePermission = exports.litellmTeamMcpManagePermission = exports.litellmTeamKnowledgebaseManagePermission = exports.litellmTeamMembersManagePermission = exports.litellmTeamManagePermission = exports.litellmTeamCreatePermission = exports.litellmAuditReadPermission = exports.litellmKeyUnblockPermission = exports.litellmKeyResetSpendPermission = exports.litellmKeyManagePermission = exports.litellmKeyRevokePermission = exports.litellmKeyCreatePermission = exports.litellmPermissions = exports.bridgeListKeys = exports.getOrProvisionUserFromClaims = exports.resolveBridgeUserId = exports.readBridgeConfig = exports.newDefaultVerifier = exports.KeycloakJWTVerifier = exports.teamBudgetStatusFor = exports.redactTeamUsage = exports.redactTeamBudget = exports.readTeamBudgetVisibility = exports.validateTeamPatchInput = exports.validateTeamWriteInput = exports.assertTeamAdmin = exports.isTeamManagementEnabled = exports.readTeamAdminConfig = exports.ProvisioningError = exports.resolveUserRole = exports.applyRoleOverrides = exports.readRoleConfigs = exports.readProvisioningDefaults = exports.provisionUser = exports.getOrProvisionUser = exports.toLiteLLMUserId = exports.resolveUserProfile = exports.resolveUserId = exports.LiteLLMClient = exports.createRouter = exports.default = exports.litellmPlugin = void 0;
|
|
18
18
|
var plugin_1 = require("./plugin");
|
|
19
19
|
Object.defineProperty(exports, "litellmPlugin", { enumerable: true, get: function () { return plugin_1.litellmPlugin; } });
|
|
20
20
|
var plugin_2 = require("./plugin");
|
|
@@ -24,7 +24,6 @@ Object.defineProperty(exports, "createRouter", { enumerable: true, get: function
|
|
|
24
24
|
__exportStar(require("./types"), exports);
|
|
25
25
|
var client_1 = require("./client");
|
|
26
26
|
Object.defineProperty(exports, "LiteLLMClient", { enumerable: true, get: function () { return client_1.LiteLLMClient; } });
|
|
27
|
-
Object.defineProperty(exports, "normalizeRequestTags", { enumerable: true, get: function () { return client_1.normalizeRequestTags; } });
|
|
28
27
|
var provisioning_1 = require("./provisioning");
|
|
29
28
|
Object.defineProperty(exports, "resolveUserId", { enumerable: true, get: function () { return provisioning_1.resolveUserId; } });
|
|
30
29
|
Object.defineProperty(exports, "resolveUserProfile", { enumerable: true, get: function () { return provisioning_1.resolveUserProfile; } });
|
|
@@ -54,7 +53,6 @@ Object.defineProperty(exports, "readBridgeConfig", { enumerable: true, get: func
|
|
|
54
53
|
Object.defineProperty(exports, "resolveBridgeUserId", { enumerable: true, get: function () { return bridge_1.resolveBridgeUserId; } });
|
|
55
54
|
Object.defineProperty(exports, "getOrProvisionUserFromClaims", { enumerable: true, get: function () { return bridge_1.getOrProvisionUserFromClaims; } });
|
|
56
55
|
Object.defineProperty(exports, "bridgeListKeys", { enumerable: true, get: function () { return bridge_1.bridgeListKeys; } });
|
|
57
|
-
Object.defineProperty(exports, "bridgeGenerateKey", { enumerable: true, get: function () { return bridge_1.bridgeGenerateKey; } });
|
|
58
56
|
var backstage_plugin_litellm_common_1 = require("@acarmisc/backstage-plugin-litellm-common");
|
|
59
57
|
Object.defineProperty(exports, "litellmPermissions", { enumerable: true, get: function () { return backstage_plugin_litellm_common_1.litellmPermissions; } });
|
|
60
58
|
Object.defineProperty(exports, "litellmKeyCreatePermission", { enumerable: true, get: function () { return backstage_plugin_litellm_common_1.litellmKeyCreatePermission; } });
|
package/dist/provisioning.d.ts
CHANGED
|
@@ -49,11 +49,13 @@ export declare function readProvisioningDefaults(config: Config): {
|
|
|
49
49
|
* Extracts the authenticated Backstage user identity from the request token.
|
|
50
50
|
* Returns the userEntityRef (e.g. "user:default/john.doe") or undefined when
|
|
51
51
|
* the request carries no user credential (service-to-service calls).
|
|
52
|
+
* Only accepts headers starting with 'Bearer ' (case-sensitive as Backstage sends it).
|
|
52
53
|
*/
|
|
53
54
|
export declare function resolveUserId(req: Request, auth: AuthService): Promise<string | undefined>;
|
|
54
55
|
/**
|
|
55
56
|
* Like resolveUserId, but returns the raw BackstageCredentials object
|
|
56
57
|
* (rather than just the entity ref) for passing into PermissionsService.authorize().
|
|
58
|
+
* Only accepts headers starting with 'Bearer ' (case-sensitive as Backstage sends it).
|
|
57
59
|
*/
|
|
58
60
|
export declare function resolveCredentials(req: Request, auth: AuthService): Promise<BackstageCredentials | undefined>;
|
|
59
61
|
/**
|
package/dist/provisioning.js
CHANGED
|
@@ -13,6 +13,7 @@ exports.getOrProvisionUser = getOrProvisionUser;
|
|
|
13
13
|
exports.isUserMemberOfGroup = isUserMemberOfGroup;
|
|
14
14
|
exports.resolveUserRole = resolveUserRole;
|
|
15
15
|
const client_1 = require("./client");
|
|
16
|
+
const errors_1 = require("./errors");
|
|
16
17
|
/**
|
|
17
18
|
* Converts a Backstage user entity ref to a LiteLLM user_id.
|
|
18
19
|
*
|
|
@@ -107,11 +108,13 @@ function readProvisioningDefaults(config) {
|
|
|
107
108
|
* Extracts the authenticated Backstage user identity from the request token.
|
|
108
109
|
* Returns the userEntityRef (e.g. "user:default/john.doe") or undefined when
|
|
109
110
|
* the request carries no user credential (service-to-service calls).
|
|
111
|
+
* Only accepts headers starting with 'Bearer ' (case-sensitive as Backstage sends it).
|
|
110
112
|
*/
|
|
111
113
|
async function resolveUserId(req, auth) {
|
|
112
|
-
const
|
|
113
|
-
if (!
|
|
114
|
+
const authHeader = req.headers.authorization ?? '';
|
|
115
|
+
if (!authHeader.startsWith('Bearer '))
|
|
114
116
|
return undefined;
|
|
117
|
+
const rawToken = authHeader.slice(7);
|
|
115
118
|
try {
|
|
116
119
|
const credentials = await auth.authenticate(rawToken);
|
|
117
120
|
const principal = credentials.principal;
|
|
@@ -120,18 +123,20 @@ async function resolveUserId(req, auth) {
|
|
|
120
123
|
}
|
|
121
124
|
}
|
|
122
125
|
catch {
|
|
123
|
-
// invalid or service token
|
|
126
|
+
// invalid or service token
|
|
124
127
|
}
|
|
125
128
|
return undefined;
|
|
126
129
|
}
|
|
127
130
|
/**
|
|
128
131
|
* Like resolveUserId, but returns the raw BackstageCredentials object
|
|
129
132
|
* (rather than just the entity ref) for passing into PermissionsService.authorize().
|
|
133
|
+
* Only accepts headers starting with 'Bearer ' (case-sensitive as Backstage sends it).
|
|
130
134
|
*/
|
|
131
135
|
async function resolveCredentials(req, auth) {
|
|
132
|
-
const
|
|
133
|
-
if (!
|
|
136
|
+
const authHeader = req.headers.authorization ?? '';
|
|
137
|
+
if (!authHeader.startsWith('Bearer '))
|
|
134
138
|
return undefined;
|
|
139
|
+
const rawToken = authHeader.slice(7);
|
|
135
140
|
try {
|
|
136
141
|
return await auth.authenticate(rawToken);
|
|
137
142
|
}
|
|
@@ -236,20 +241,6 @@ async function provisionUser(client, userId, defaults, profile, backstageEntity,
|
|
|
236
241
|
* requests for a re-deleted user can still trigger fresh provisioning.
|
|
237
242
|
*/
|
|
238
243
|
const provisioningInFlight = new Map();
|
|
239
|
-
/**
|
|
240
|
-
* Strips any echoed Authorization bearer token from upstream LiteLLM error
|
|
241
|
-
* messages before they're shipped back to the browser. LiteLLM normally does
|
|
242
|
-
* not echo the master key, but defense in depth: never let a `Bearer …`
|
|
243
|
-
* substring travel out in a response body.
|
|
244
|
-
*/
|
|
245
|
-
function sanitizeUpstreamMessage(message) {
|
|
246
|
-
if (!message)
|
|
247
|
-
return 'unknown error';
|
|
248
|
-
return message
|
|
249
|
-
.replace(/Bearer\s+[A-Za-z0-9._\-+/=]+/g, 'Bearer [redacted]')
|
|
250
|
-
.replace(/sk-[A-Za-z0-9_\-]{8,}/g, 'sk-[redacted]')
|
|
251
|
-
.slice(0, 500);
|
|
252
|
-
}
|
|
253
244
|
class ProvisioningError extends Error {
|
|
254
245
|
constructor(message, hint, provisioning, status = 404) {
|
|
255
246
|
super(message);
|
|
@@ -331,7 +322,7 @@ async function getOrProvisionUser(client, tokenEntityRef, userId, provisioningEn
|
|
|
331
322
|
? upstreamStatus
|
|
332
323
|
: 502;
|
|
333
324
|
const errMessage = err instanceof Error ? err.message : String(err);
|
|
334
|
-
throw new ProvisioningError('LiteLLM auto-provisioning failed', `LiteLLM upstream ${upstreamStatus ?? 'error'}: ${sanitizeUpstreamMessage(errMessage)}`, true, passThrough);
|
|
325
|
+
throw new ProvisioningError('LiteLLM auto-provisioning failed', `LiteLLM upstream ${upstreamStatus ?? 'error'}: ${(0, errors_1.sanitizeUpstreamMessage)(errMessage)}`, true, passThrough);
|
|
335
326
|
}
|
|
336
327
|
})();
|
|
337
328
|
provisioningInFlight.set(userId, provisionPromise);
|
package/dist/router.js
CHANGED
|
@@ -148,22 +148,6 @@ async function createRouter(options) {
|
|
|
148
148
|
}
|
|
149
149
|
return { tokenEntityRef, userId, key };
|
|
150
150
|
},
|
|
151
|
-
sendOwnershipError: (err, res) => {
|
|
152
|
-
// Handle NotAllowedError from authorizeKeyAction
|
|
153
|
-
if (err instanceof errors_1.NotAllowedError) {
|
|
154
|
-
res.status(403).json({ error: err.message });
|
|
155
|
-
return true;
|
|
156
|
-
}
|
|
157
|
-
// Fallback for legacy errors with status and body
|
|
158
|
-
if (err && typeof err === 'object' && 'status' in err && 'body' in err) {
|
|
159
|
-
const errObj = err;
|
|
160
|
-
if (typeof errObj.status === 'number' && errObj.body) {
|
|
161
|
-
res.status(errObj.status).json(errObj.body);
|
|
162
|
-
return true;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
return false;
|
|
166
|
-
},
|
|
167
151
|
assertPermission: async (req, permission) => {
|
|
168
152
|
const credentials = await (0, provisioning_1.resolveCredentials)(req, auth);
|
|
169
153
|
if (!credentials)
|
|
@@ -201,13 +185,19 @@ async function createRouter(options) {
|
|
|
201
185
|
sendTeamError: (err, res) => {
|
|
202
186
|
(0, errors_2.sendError)(res, err, logger, 'team operation');
|
|
203
187
|
},
|
|
204
|
-
authorizeTeamSubresource: async (req, res, permission) => {
|
|
188
|
+
authorizeTeamSubresource: async (req, res, permission, opts) => {
|
|
205
189
|
if (!ctx.requireTeamMgmt(res))
|
|
206
190
|
return null;
|
|
207
191
|
if (!teamAdminCfg.group) {
|
|
208
192
|
res.status(500).json({ error: 'Team management is misconfigured (group is missing)' });
|
|
209
193
|
return null;
|
|
210
194
|
}
|
|
195
|
+
if (teamAdminCfg.readOnly) {
|
|
196
|
+
res.status(403).json({
|
|
197
|
+
error: 'Team management is read-only (litellm.teamAdmin.readOnly): teams are managed outside Backstage',
|
|
198
|
+
});
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
211
201
|
const check = await (0, teamAdmin_1.assertTeamAdmin)({
|
|
212
202
|
req,
|
|
213
203
|
auth,
|
|
@@ -217,7 +207,11 @@ async function createRouter(options) {
|
|
|
217
207
|
permission,
|
|
218
208
|
logger,
|
|
219
209
|
});
|
|
220
|
-
|
|
210
|
+
// A caller outside the admin group may still qualify through their
|
|
211
|
+
// LiteLLM team role (litellm.teamAdmin.memberManagerRoles), so a 403
|
|
212
|
+
// here is deferred until that path has been tried.
|
|
213
|
+
const teamRoleEligible = !!opts?.allowTeamRole && teamAdminCfg.memberManagerRoles.length > 0;
|
|
214
|
+
if (!check.ok && !(check.status === 403 && teamRoleEligible)) {
|
|
221
215
|
res.status(check.status).json({ error: check.error });
|
|
222
216
|
return null;
|
|
223
217
|
}
|
|
@@ -241,25 +235,53 @@ async function createRouter(options) {
|
|
|
241
235
|
const owningGroup = typeof existing.metadata?.owning_group === 'string'
|
|
242
236
|
? existing.metadata.owning_group
|
|
243
237
|
: undefined;
|
|
244
|
-
|
|
245
|
-
|
|
238
|
+
// Group path: admin-group member whose group owns the team.
|
|
239
|
+
let denial;
|
|
240
|
+
if (!check.ok) {
|
|
241
|
+
denial = { status: check.status, error: check.error };
|
|
242
|
+
}
|
|
243
|
+
else if (!owningGroup) {
|
|
244
|
+
denial = {
|
|
245
|
+
status: 403,
|
|
246
246
|
error: 'This team is not managed by Backstage team admins and cannot be edited here',
|
|
247
|
-
}
|
|
248
|
-
return null;
|
|
247
|
+
};
|
|
249
248
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
249
|
+
else if (await (0, provisioning_1.isUserMemberOfGroup)(check.userEntityRef, owningGroup, catalogClient, auth, logger)) {
|
|
250
|
+
return {
|
|
251
|
+
teamId,
|
|
252
|
+
owningGroup,
|
|
253
|
+
actor: check.userEntityRef,
|
|
254
|
+
team: existing,
|
|
255
|
+
via: 'group',
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
denial = {
|
|
260
|
+
status: 403,
|
|
261
|
+
error: `Access denied: team is owned by ${owningGroup}`,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
// Team-role path: the caller holds an allowed role in this very team.
|
|
265
|
+
// The permission framework still has the final say.
|
|
266
|
+
if (teamRoleEligible) {
|
|
267
|
+
const actor = await (0, provisioning_1.resolveUserId)(req, auth);
|
|
268
|
+
const litellmUserId = actor && (0, provisioning_1.toLiteLLMUserId)(actor, userIdDomain);
|
|
269
|
+
const role = existing.members_with_roles?.find(m => m.user_id === litellmUserId)?.role;
|
|
270
|
+
if (actor &&
|
|
271
|
+
role &&
|
|
272
|
+
teamAdminCfg.memberManagerRoles.includes(role) &&
|
|
273
|
+
(await ctx.assertPermission(req, permission))) {
|
|
274
|
+
return {
|
|
275
|
+
teamId,
|
|
276
|
+
owningGroup: owningGroup ?? '',
|
|
277
|
+
actor,
|
|
278
|
+
team: existing,
|
|
279
|
+
via: 'teamRole',
|
|
280
|
+
};
|
|
281
|
+
}
|
|
256
282
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
owningGroup,
|
|
260
|
-
actor: check.userEntityRef,
|
|
261
|
-
team: existing,
|
|
262
|
-
};
|
|
283
|
+
res.status(denial.status).json({ error: denial.error });
|
|
284
|
+
return null;
|
|
263
285
|
},
|
|
264
286
|
};
|
|
265
287
|
const router = (0, express_1.Router)();
|
package/dist/routes/bridge.js
CHANGED
|
@@ -8,7 +8,7 @@ const keyService_1 = require("../services/keyService");
|
|
|
8
8
|
const errors_1 = require("../errors");
|
|
9
9
|
const backstage_plugin_litellm_common_2 = require("@acarmisc/backstage-plugin-litellm-common");
|
|
10
10
|
function registerBridgeRoutes(router, ctx, bridgeOpts) {
|
|
11
|
-
const { client, logger, userIdDomain, provisioningEnabled, provisioningDefaults,
|
|
11
|
+
const { client, logger, userIdDomain, provisioningEnabled, provisioningDefaults, allowUnlimitedBudget, teamRequired, keyValidationConfig, } = ctx;
|
|
12
12
|
const { config } = bridgeOpts;
|
|
13
13
|
const generateKeyInputSchema = (0, backstage_plugin_litellm_common_2.createGenerateKeyInputSchema)(ctx.keyValidationConfig);
|
|
14
14
|
// ---------------------------------------------------------------------------
|
|
@@ -99,8 +99,8 @@ function registerBridgeRoutes(router, ctx, bridgeOpts) {
|
|
|
99
99
|
if (!allowUnlimitedBudget && input.max_budget === undefined) {
|
|
100
100
|
input.max_budget = provisioningDefaults.maxBudget;
|
|
101
101
|
}
|
|
102
|
-
// ── Resolve user
|
|
103
|
-
const
|
|
102
|
+
// ── Resolve and provision user from verified claims ──────────────────
|
|
103
|
+
const userInfo = await (0, bridge_1.getOrProvisionUserFromClaims)(client, claims, provisioningEnabled, provisioningDefaults, logger, bridgeIdentity);
|
|
104
104
|
// ── Create key via unified service ──────────────────────────────────
|
|
105
105
|
const keyCreateCtx = {
|
|
106
106
|
client,
|
|
@@ -108,18 +108,10 @@ function registerBridgeRoutes(router, ctx, bridgeOpts) {
|
|
|
108
108
|
allowUnlimitedBudget,
|
|
109
109
|
teamRequired,
|
|
110
110
|
},
|
|
111
|
-
// Bridge does not have Backstage catalogClient/auth
|
|
112
|
-
// so provisioning from JWT claims only (no Backstage profile enrichment)
|
|
113
|
-
catalogClient: undefined,
|
|
114
|
-
auth: undefined,
|
|
115
111
|
logger,
|
|
116
112
|
keyValidationConfig,
|
|
117
|
-
provisioningEnabled,
|
|
118
|
-
provisioningDefaults,
|
|
119
|
-
roleConfigs,
|
|
120
|
-
userIdDomain,
|
|
121
113
|
};
|
|
122
|
-
const result = await (0, keyService_1.createKeyForUser)({
|
|
114
|
+
const result = await (0, keyService_1.createKeyForUser)({ userInfo, createdVia: 'abby-cli' }, input, keyCreateCtx);
|
|
123
115
|
res.json(result);
|
|
124
116
|
}
|
|
125
117
|
catch (error) {
|
|
@@ -132,23 +124,22 @@ function registerBridgeRoutes(router, ctx, bridgeOpts) {
|
|
|
132
124
|
try {
|
|
133
125
|
const claims = await requireClaims(req);
|
|
134
126
|
const user = await (0, bridge_1.getOrProvisionUserFromClaims)(client, claims, provisioningEnabled, provisioningDefaults, logger, bridgeIdentity);
|
|
135
|
-
// Enrich team list with metadata (display names, etc.)
|
|
127
|
+
// Enrich team list with metadata (display names, etc.) in parallel
|
|
136
128
|
const teamIds = user.teams ?? [];
|
|
137
|
-
const teamMetadata =
|
|
138
|
-
for (const teamId of teamIds) {
|
|
129
|
+
const teamMetadata = await Promise.all(teamIds.map(async (teamId) => {
|
|
139
130
|
try {
|
|
140
131
|
const teamInfo = await client.getTeamInfo(teamId);
|
|
141
|
-
|
|
132
|
+
return {
|
|
142
133
|
id: teamId,
|
|
143
134
|
name: teamInfo.team_alias || teamId, // Use alias as display name, fallback to ID
|
|
144
|
-
}
|
|
135
|
+
};
|
|
145
136
|
}
|
|
146
137
|
catch (e) {
|
|
147
138
|
// If we can't fetch team info, just use the ID
|
|
148
139
|
logger.debug(`Could not fetch team info for ${teamId}: ${e.message}`);
|
|
149
|
-
|
|
140
|
+
return { id: teamId };
|
|
150
141
|
}
|
|
151
|
-
}
|
|
142
|
+
}));
|
|
152
143
|
res.json({ user_id: user.user_id, teams: teamIds, team_metadata: teamMetadata });
|
|
153
144
|
}
|
|
154
145
|
catch (error) {
|
package/dist/routes/config.js
CHANGED
|
@@ -26,6 +26,8 @@ function registerConfigRoutes(router, ctx) {
|
|
|
26
26
|
maxBudgetCeiling: ctx.teamAdminCfg.maxBudgetCeiling,
|
|
27
27
|
allowUnlimitedBudget: ctx.teamAdminCfg.allowUnlimitedBudget,
|
|
28
28
|
objectPermissionsEnabled: ctx.objectPermsEnabled,
|
|
29
|
+
readOnly: ctx.teamAdminCfg.readOnly,
|
|
30
|
+
memberManagerRoles: ctx.teamAdminCfg.memberManagerRoles,
|
|
29
31
|
},
|
|
30
32
|
display: {
|
|
31
33
|
hideTeamBudgetForMembers: ctx.teamBudgetVisibility.hideTeamBudgetForMembers,
|
package/dist/routes/context.d.ts
CHANGED
|
@@ -40,16 +40,18 @@ export interface RouterContext {
|
|
|
40
40
|
userId: string;
|
|
41
41
|
key: VirtualKey;
|
|
42
42
|
}>;
|
|
43
|
-
sendOwnershipError(err: unknown, res: Response): boolean;
|
|
44
43
|
assertPermission(req: Request, permission: BasicPermission): Promise<boolean>;
|
|
45
44
|
sendPermissionDenied(res: Response, permission: BasicPermission): void;
|
|
46
45
|
requireTeamMgmt(res: Response): boolean;
|
|
47
46
|
requireObjectPerms(res: Response): boolean;
|
|
48
47
|
sendTeamError(err: unknown, res: Response): void;
|
|
49
|
-
authorizeTeamSubresource(req: Request, res: Response, permission: BasicPermission
|
|
48
|
+
authorizeTeamSubresource(req: Request, res: Response, permission: BasicPermission, opts?: {
|
|
49
|
+
allowTeamRole?: boolean;
|
|
50
|
+
}): Promise<{
|
|
50
51
|
teamId: string;
|
|
51
52
|
owningGroup: string;
|
|
52
53
|
actor: string;
|
|
53
54
|
team: TeamInfo;
|
|
55
|
+
via: 'group' | 'teamRole';
|
|
54
56
|
} | null>;
|
|
55
57
|
}
|
package/dist/routes/keys.js
CHANGED
|
@@ -13,7 +13,7 @@ const withUser_1 = require("./middleware/withUser");
|
|
|
13
13
|
* location and access to shared state (via ctx) differ.
|
|
14
14
|
*/
|
|
15
15
|
function registerKeysRoutes(router, ctx) {
|
|
16
|
-
const { client, catalogClient, auth, logger,
|
|
16
|
+
const { client, catalogClient, auth, logger, auditGroup, allowUnlimitedBudget, teamRequired, allowOwnerResetSpend, keyValidationConfig, authorizeKeyAction, assertPermission, sendPermissionDenied, } = ctx;
|
|
17
17
|
const generateKeyInputSchema = (0, backstage_plugin_litellm_common_1.createGenerateKeyInputSchema)(keyValidationConfig);
|
|
18
18
|
const updateKeyInputSchema = (0, backstage_plugin_litellm_common_1.createUpdateKeyInputSchema)(keyValidationConfig);
|
|
19
19
|
router.get('/user/info', async (_req, res) => {
|
|
@@ -132,7 +132,8 @@ function registerKeysRoutes(router, ctx) {
|
|
|
132
132
|
return;
|
|
133
133
|
}
|
|
134
134
|
const tokenEntityRef = res.locals.tokenEntityRef;
|
|
135
|
-
|
|
135
|
+
// ── Resolve and provision user ──────────────────────────────────────
|
|
136
|
+
const userInfo = await (0, withUser_1.getProvisionedUser)(ctx, res);
|
|
136
137
|
// ── Create key via unified service ──────────────────────────────────
|
|
137
138
|
const keyCreateCtx = {
|
|
138
139
|
client,
|
|
@@ -144,12 +145,8 @@ function registerKeysRoutes(router, ctx) {
|
|
|
144
145
|
auth,
|
|
145
146
|
logger,
|
|
146
147
|
keyValidationConfig,
|
|
147
|
-
provisioningEnabled,
|
|
148
|
-
provisioningDefaults,
|
|
149
|
-
roleConfigs,
|
|
150
|
-
userIdDomain,
|
|
151
148
|
};
|
|
152
|
-
const result = await (0, keyService_1.createKeyForUser)({ tokenEntityRef,
|
|
149
|
+
const result = await (0, keyService_1.createKeyForUser)({ tokenEntityRef, userInfo }, input, keyCreateCtx);
|
|
153
150
|
res.json(result);
|
|
154
151
|
}
|
|
155
152
|
catch (error) {
|
|
@@ -277,8 +274,6 @@ function registerKeysRoutes(router, ctx) {
|
|
|
277
274
|
res.json(result);
|
|
278
275
|
}
|
|
279
276
|
catch (error) {
|
|
280
|
-
if (sendOwnershipError(error, res))
|
|
281
|
-
return;
|
|
282
277
|
(0, errors_1.sendError)(res, error, logger, 'update key');
|
|
283
278
|
}
|
|
284
279
|
});
|
|
@@ -299,8 +294,6 @@ function registerKeysRoutes(router, ctx) {
|
|
|
299
294
|
res.json({ success: true });
|
|
300
295
|
}
|
|
301
296
|
catch (error) {
|
|
302
|
-
if (sendOwnershipError(error, res))
|
|
303
|
-
return;
|
|
304
297
|
(0, errors_1.sendError)(res, error, logger, 'delete key');
|
|
305
298
|
}
|
|
306
299
|
});
|
|
@@ -334,8 +327,6 @@ function registerKeysRoutes(router, ctx) {
|
|
|
334
327
|
res.json({ success: true });
|
|
335
328
|
}
|
|
336
329
|
catch (error) {
|
|
337
|
-
if (sendOwnershipError(error, res))
|
|
338
|
-
return;
|
|
339
330
|
(0, errors_1.sendError)(res, error, logger, 'block key');
|
|
340
331
|
}
|
|
341
332
|
});
|
|
@@ -371,8 +362,6 @@ function registerKeysRoutes(router, ctx) {
|
|
|
371
362
|
res.json({ success: true });
|
|
372
363
|
}
|
|
373
364
|
catch (error) {
|
|
374
|
-
if (sendOwnershipError(error, res))
|
|
375
|
-
return;
|
|
376
365
|
(0, errors_1.sendError)(res, error, logger, 'unblock key');
|
|
377
366
|
}
|
|
378
367
|
});
|
|
@@ -397,8 +386,6 @@ function registerKeysRoutes(router, ctx) {
|
|
|
397
386
|
res.json({ success: true });
|
|
398
387
|
}
|
|
399
388
|
catch (error) {
|
|
400
|
-
if (sendOwnershipError(error, res))
|
|
401
|
-
return;
|
|
402
389
|
(0, errors_1.sendError)(res, error, logger, 'reset key spend');
|
|
403
390
|
}
|
|
404
391
|
});
|
|
@@ -454,10 +441,10 @@ function registerKeysRoutes(router, ctx) {
|
|
|
454
441
|
res.status(400).json({ error: 'start_date and end_date are required' });
|
|
455
442
|
return;
|
|
456
443
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
const usage = await client.getUsage(start_date, end_date, userId);
|
|
444
|
+
await (0, withUser_1.getProvisionedUser)(ctx, res);
|
|
445
|
+
// Always filter by the caller's id: an empty user_id makes LiteLLM
|
|
446
|
+
// return org-wide activity.
|
|
447
|
+
const usage = await client.getUsage(start_date, end_date, res.locals.userId);
|
|
461
448
|
res.json(usage);
|
|
462
449
|
}
|
|
463
450
|
catch (error) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
import { UserInfo } from '../../types';
|
|
2
3
|
import type { RouterContext } from '../context';
|
|
3
4
|
/**
|
|
4
5
|
* Express middleware that resolves a Backstage user principal to a LiteLLM user ID.
|
|
@@ -12,7 +13,7 @@ export declare function createRequireUser(ctx: RouterContext): (req: Request, re
|
|
|
12
13
|
* Returns the user info or throws ProvisioningError (which the caller must handle and respond to).
|
|
13
14
|
*
|
|
14
15
|
* This deduplicates the repeated pattern:
|
|
15
|
-
* const userInfo = await
|
|
16
|
+
* const userInfo = await getProvisionedUser(...);
|
|
16
17
|
* if (error instanceof ProvisioningError) { res.status(...).json(...); return; }
|
|
17
18
|
*/
|
|
18
|
-
export declare function getProvisionedUser(ctx: RouterContext, res: Response): Promise<
|
|
19
|
+
export declare function getProvisionedUser(ctx: RouterContext, res: Response): Promise<UserInfo>;
|
|
@@ -31,7 +31,7 @@ function createRequireUser(ctx) {
|
|
|
31
31
|
* Returns the user info or throws ProvisioningError (which the caller must handle and respond to).
|
|
32
32
|
*
|
|
33
33
|
* This deduplicates the repeated pattern:
|
|
34
|
-
* const userInfo = await
|
|
34
|
+
* const userInfo = await getProvisionedUser(...);
|
|
35
35
|
* if (error instanceof ProvisioningError) { res.status(...).json(...); return; }
|
|
36
36
|
*/
|
|
37
37
|
async function getProvisionedUser(ctx, res) {
|
|
@@ -3,9 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.registerObjectPermissionsRoutes = registerObjectPermissionsRoutes;
|
|
4
4
|
const backstage_plugin_litellm_common_1 = require("@acarmisc/backstage-plugin-litellm-common");
|
|
5
5
|
const teamAdmin_1 = require("../teamAdmin");
|
|
6
|
-
const
|
|
6
|
+
const respondTeam_1 = require("../http/respondTeam");
|
|
7
7
|
function registerObjectPermissionsRoutes(router, ctx) {
|
|
8
|
-
const { client, catalogClient, auth, permissions, logger, teamAdminCfg,
|
|
8
|
+
const { client, catalogClient, auth, permissions, logger, teamAdminCfg, requireObjectPerms, sendTeamError, authorizeTeamSubresource, } = ctx;
|
|
9
9
|
// ── Knowledge-base (vector store) management ────────────────────────────
|
|
10
10
|
//
|
|
11
11
|
// Attaching a vector store to a team exposes its documents to every team
|
|
@@ -91,9 +91,7 @@ function registerObjectPermissionsRoutes(router, ctx) {
|
|
|
91
91
|
owningGroup,
|
|
92
92
|
vector_stores: requested,
|
|
93
93
|
});
|
|
94
|
-
|
|
95
|
-
? (0, teamBudgetVisibility_1.redactTeamBudget)(updated)
|
|
96
|
-
: updated);
|
|
94
|
+
(0, respondTeam_1.respondTeam)(res, updated, ctx, 'manager');
|
|
97
95
|
}
|
|
98
96
|
catch (err) {
|
|
99
97
|
sendTeamError(err, res);
|
|
@@ -185,9 +183,7 @@ function registerObjectPermissionsRoutes(router, ctx) {
|
|
|
185
183
|
before,
|
|
186
184
|
after: requested,
|
|
187
185
|
});
|
|
188
|
-
|
|
189
|
-
? (0, teamBudgetVisibility_1.redactTeamBudget)(updated)
|
|
190
|
-
: updated);
|
|
186
|
+
(0, respondTeam_1.respondTeam)(res, updated, ctx, 'manager');
|
|
191
187
|
}
|
|
192
188
|
catch (err) {
|
|
193
189
|
sendTeamError(err, res);
|
package/dist/routes/teamUsage.js
CHANGED
|
@@ -7,7 +7,7 @@ const errors_1 = require("../errors");
|
|
|
7
7
|
const teamFetch_1 = require("../http/teamFetch");
|
|
8
8
|
const withUser_1 = require("./middleware/withUser");
|
|
9
9
|
function registerTeamUsageRoutes(router, ctx) {
|
|
10
|
-
const { client, catalogClient, auth, logger,
|
|
10
|
+
const { client, catalogClient, auth, logger, teamBudgetVisibility, } = ctx;
|
|
11
11
|
const requireUser = (0, withUser_1.createRequireUser)(ctx);
|
|
12
12
|
router.get('/teams/:teamId/usage', requireUser, async (req, res) => {
|
|
13
13
|
try {
|
|
@@ -19,8 +19,7 @@ function registerTeamUsageRoutes(router, ctx) {
|
|
|
19
19
|
}
|
|
20
20
|
// Authorize: member of the team OR a team manager
|
|
21
21
|
const tokenEntityRef = res.locals.tokenEntityRef;
|
|
22
|
-
const
|
|
23
|
-
const userInfo = await (0, provisioning_1.getOrProvisionUser)(client, tokenEntityRef, userId, provisioningEnabled, provisioningDefaults, roleConfigs, catalogClient, auth, logger);
|
|
22
|
+
const userInfo = await (0, withUser_1.getProvisionedUser)(ctx, res);
|
|
24
23
|
// Check membership: user must be in the team OR be a team manager
|
|
25
24
|
const isMember = userInfo?.teams?.includes(teamId) ?? false;
|
|
26
25
|
// A "manager" administers this specific team: team management is enabled
|