@acarmisc/backstage-plugin-litellm-backend 0.16.0 → 0.16.1
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/config.d.ts +9 -0
- package/dist/bridge.d.ts +32 -13
- package/dist/bridge.js +35 -19
- package/dist/index.cjs.js +27 -9
- package/dist/index.cjs.js.map +2 -2
- package/dist/routes/bridge.js +7 -2
- package/package.json +1 -1
package/config.d.ts
CHANGED
|
@@ -390,6 +390,15 @@ export interface Config {
|
|
|
390
390
|
* @default "abby-cli"
|
|
391
391
|
*/
|
|
392
392
|
clientId?: string;
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Email domains whose *verified* addresses may use the bridge. Defaults
|
|
396
|
+
* to `litellm.userIdDomain` when that is set. With neither configured
|
|
397
|
+
* the bridge rejects every caller. The LiteLLM user is then the same one
|
|
398
|
+
* the UI addresses (the Keycloak `preferred_username`, with
|
|
399
|
+
* `litellm.userIdDomain` applied when set) — the email is only the gate.
|
|
400
|
+
*/
|
|
401
|
+
allowedEmailDomains?: string[];
|
|
393
402
|
};
|
|
394
403
|
};
|
|
395
404
|
}
|
package/dist/bridge.d.ts
CHANGED
|
@@ -25,6 +25,12 @@ export interface BridgeConfig {
|
|
|
25
25
|
issuer?: string;
|
|
26
26
|
/** OIDC public client the CLI uses; checked against azp / aud. */
|
|
27
27
|
clientId: string;
|
|
28
|
+
/**
|
|
29
|
+
* Email domains whose verified addresses may use the bridge. Defaults to
|
|
30
|
+
* `litellm.userIdDomain` when that is set; with neither, the bridge rejects
|
|
31
|
+
* every caller (fail closed).
|
|
32
|
+
*/
|
|
33
|
+
allowedEmailDomains: string[];
|
|
28
34
|
}
|
|
29
35
|
export declare function readBridgeConfig(config: Config): BridgeConfig;
|
|
30
36
|
/** Thrown when the bridge is misconfigured (e.g. enabled without an issuer). */
|
|
@@ -57,28 +63,41 @@ export declare class KeycloakJWTVerifier implements TokenVerifier {
|
|
|
57
63
|
/** Builds the default verifier from config, or throws BridgeConfigError. */
|
|
58
64
|
export declare function newDefaultVerifier(cfg: BridgeConfig): TokenVerifier;
|
|
59
65
|
/**
|
|
60
|
-
*
|
|
66
|
+
* How the bridge maps a verified token to a LiteLLM user.
|
|
67
|
+
* - `userIdDomain`: the same `litellm.userIdDomain` the UI uses to build ids.
|
|
68
|
+
* - `trustedEmailDomains`: email domains allowed to use the bridge; defaults to
|
|
69
|
+
* `[userIdDomain]` when that is set.
|
|
70
|
+
*/
|
|
71
|
+
export interface BridgeIdentityOptions {
|
|
72
|
+
userIdDomain?: string;
|
|
73
|
+
trustedEmailDomains?: string[];
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Resolves the LiteLLM user_id for a verified bridge token.
|
|
61
77
|
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
78
|
+
* Two separate questions, answered separately:
|
|
79
|
+
* 1. Is this person allowed in? The token must carry a *verified* email whose
|
|
80
|
+
* domain is in the trusted list (`litellm.bridge.allowedEmailDomains`,
|
|
81
|
+
* defaulting to `litellm.userIdDomain`). With no trusted domain configured
|
|
82
|
+
* everyone is rejected (fail closed). Failure → 403.
|
|
83
|
+
* 2. Which LiteLLM user are they? The same one the UI addresses: the Backstage
|
|
84
|
+
* user entity name (the Keycloak `preferred_username`) run through the same
|
|
85
|
+
* `litellm.userIdDomain` rule the UI applies. Using the email local part
|
|
86
|
+
* here would address a different user whenever a Keycloak username differs
|
|
87
|
+
* from the email (and would provision a duplicate). The realm is the same
|
|
88
|
+
* IdP Backstage signs users in with, so its usernames are the entity names.
|
|
67
89
|
*
|
|
68
|
-
* @param claims Verified JWT claims from the bridge token
|
|
69
|
-
* @param userIdDomain Expected email domain (e.g. "example.com"); if set, email must match
|
|
70
|
-
* @returns The resolved LiteLLM user_id
|
|
71
90
|
* @throws BridgeIdentityError (403) if identity validation fails
|
|
72
91
|
*/
|
|
73
|
-
export declare function resolveBridgeUserId(claims: BridgeClaims,
|
|
92
|
+
export declare function resolveBridgeUserId(claims: BridgeClaims, identity?: string | BridgeIdentityOptions): string;
|
|
74
93
|
/**
|
|
75
94
|
* Ensures a LiteLLM user exists for the verified identity. If the user is
|
|
76
95
|
* missing and provisioning is enabled, creates it from the JWT claims (email +
|
|
77
96
|
* name); if provisioning is disabled, throws a 404 telling the caller to log
|
|
78
97
|
* in to Backstage first (the UI is the primary provisioning entry point).
|
|
79
98
|
*/
|
|
80
|
-
export declare function getOrProvisionUserFromClaims(client: LiteLLMClient, claims: BridgeClaims, provisioningEnabled: boolean, provisioningDefaults: ProvisioningDefaults, logger: LoggerService, userIdDomain?: string): Promise<UserInfo>;
|
|
99
|
+
export declare function getOrProvisionUserFromClaims(client: LiteLLMClient, claims: BridgeClaims, provisioningEnabled: boolean, provisioningDefaults: ProvisioningDefaults, logger: LoggerService, userIdDomain?: string | BridgeIdentityOptions): Promise<UserInfo>;
|
|
81
100
|
/** Lists the caller's virtual keys (provisioning the user first if needed). */
|
|
82
|
-
export declare function bridgeListKeys(client: LiteLLMClient, claims: BridgeClaims, provisioningEnabled: boolean, provisioningDefaults: ProvisioningDefaults, logger: LoggerService, userIdDomain?: string): Promise<VirtualKey[]>;
|
|
101
|
+
export declare function bridgeListKeys(client: LiteLLMClient, claims: BridgeClaims, provisioningEnabled: boolean, provisioningDefaults: ProvisioningDefaults, logger: LoggerService, userIdDomain?: string | BridgeIdentityOptions): Promise<VirtualKey[]>;
|
|
83
102
|
/** Mints a new virtual key for the caller (provisioning the user first if needed). */
|
|
84
|
-
export declare function bridgeGenerateKey(client: LiteLLMClient, claims: BridgeClaims, provisioningEnabled: boolean, provisioningDefaults: ProvisioningDefaults, logger: LoggerService, request: Partial<GenerateKeyRequest>, userIdDomain?: string): Promise<GenerateKeyResponse>;
|
|
103
|
+
export declare function bridgeGenerateKey(client: LiteLLMClient, claims: BridgeClaims, provisioningEnabled: boolean, provisioningDefaults: ProvisioningDefaults, logger: LoggerService, request: Partial<GenerateKeyRequest>, userIdDomain?: string | BridgeIdentityOptions): Promise<GenerateKeyResponse>;
|
package/dist/bridge.js
CHANGED
|
@@ -28,7 +28,8 @@ function readBridgeConfig(config) {
|
|
|
28
28
|
const enabled = config.getOptionalBoolean('litellm.bridge.enabled') ?? false;
|
|
29
29
|
const issuer = config.getOptionalString('litellm.bridge.issuer');
|
|
30
30
|
const clientId = config.getOptionalString('litellm.bridge.clientId') ?? 'abby-cli';
|
|
31
|
-
|
|
31
|
+
const allowedEmailDomains = (config.getOptionalStringArray('litellm.bridge.allowedEmailDomains') ?? []).map(d => d.trim().toLowerCase()).filter(Boolean);
|
|
32
|
+
return { enabled, issuer, clientId, allowedEmailDomains };
|
|
32
33
|
}
|
|
33
34
|
/** Thrown when the bridge is misconfigured (e.g. enabled without an issuer). */
|
|
34
35
|
class BridgeConfigError extends Error {
|
|
@@ -110,26 +111,40 @@ function newDefaultVerifier(cfg) {
|
|
|
110
111
|
}
|
|
111
112
|
return new KeycloakJWTVerifier({ issuer: cfg.issuer, clientId: cfg.clientId });
|
|
112
113
|
}
|
|
114
|
+
/** A bare string is the legacy form: `userIdDomain`, also the only trusted domain. */
|
|
115
|
+
function normalizeIdentityOptions(opts) {
|
|
116
|
+
const o = typeof opts === 'string' ? { userIdDomain: opts } : opts ?? {};
|
|
117
|
+
let domains = [];
|
|
118
|
+
if (o.trustedEmailDomains?.length) {
|
|
119
|
+
domains = o.trustedEmailDomains;
|
|
120
|
+
}
|
|
121
|
+
else if (o.userIdDomain) {
|
|
122
|
+
domains = [o.userIdDomain];
|
|
123
|
+
}
|
|
124
|
+
const trusted = domains.map(d => d.toLowerCase());
|
|
125
|
+
return { userIdDomain: o.userIdDomain, trusted };
|
|
126
|
+
}
|
|
113
127
|
/**
|
|
114
|
-
* Resolves the LiteLLM user_id
|
|
128
|
+
* Resolves the LiteLLM user_id for a verified bridge token.
|
|
115
129
|
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
130
|
+
* Two separate questions, answered separately:
|
|
131
|
+
* 1. Is this person allowed in? The token must carry a *verified* email whose
|
|
132
|
+
* domain is in the trusted list (`litellm.bridge.allowedEmailDomains`,
|
|
133
|
+
* defaulting to `litellm.userIdDomain`). With no trusted domain configured
|
|
134
|
+
* everyone is rejected (fail closed). Failure → 403.
|
|
135
|
+
* 2. Which LiteLLM user are they? The same one the UI addresses: the Backstage
|
|
136
|
+
* user entity name (the Keycloak `preferred_username`) run through the same
|
|
137
|
+
* `litellm.userIdDomain` rule the UI applies. Using the email local part
|
|
138
|
+
* here would address a different user whenever a Keycloak username differs
|
|
139
|
+
* from the email (and would provision a duplicate). The realm is the same
|
|
140
|
+
* IdP Backstage signs users in with, so its usernames are the entity names.
|
|
121
141
|
*
|
|
122
|
-
* @param claims Verified JWT claims from the bridge token
|
|
123
|
-
* @param userIdDomain Expected email domain (e.g. "example.com"); if set, email must match
|
|
124
|
-
* @returns The resolved LiteLLM user_id
|
|
125
142
|
* @throws BridgeIdentityError (403) if identity validation fails
|
|
126
143
|
*/
|
|
127
|
-
function resolveBridgeUserId(claims,
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (!userIdDomain) {
|
|
132
|
-
throw new BridgeIdentityError('litellm.userIdDomain must be configured to use the CLI bridge');
|
|
144
|
+
function resolveBridgeUserId(claims, identity) {
|
|
145
|
+
const { userIdDomain, trusted } = normalizeIdentityOptions(identity);
|
|
146
|
+
if (trusted.length === 0) {
|
|
147
|
+
throw new BridgeIdentityError('set litellm.bridge.allowedEmailDomains (or litellm.userIdDomain) to use the CLI bridge');
|
|
133
148
|
}
|
|
134
149
|
if (!claims.email) {
|
|
135
150
|
throw new BridgeIdentityError('no email found in token');
|
|
@@ -141,11 +156,12 @@ function resolveBridgeUserId(claims, userIdDomain) {
|
|
|
141
156
|
if (parts.length !== 2 || !parts[0]) {
|
|
142
157
|
throw new BridgeIdentityError('malformed email in token');
|
|
143
158
|
}
|
|
144
|
-
const [
|
|
145
|
-
if (
|
|
159
|
+
const [emailLocalPart, domain] = parts;
|
|
160
|
+
if (!trusted.includes(domain.toLowerCase())) {
|
|
146
161
|
throw new BridgeIdentityError(`email domain mismatch: "${domain}" is not allowed for this deployment`);
|
|
147
162
|
}
|
|
148
|
-
|
|
163
|
+
const name = claims.preferred_username?.trim() || emailLocalPart;
|
|
164
|
+
return (0, provisioning_1.toLiteLLMUserId)(name, userIdDomain);
|
|
149
165
|
}
|
|
150
166
|
/**
|
|
151
167
|
* Ensures a LiteLLM user exists for the verified identity. If the user is
|
package/dist/index.cjs.js
CHANGED
|
@@ -12616,7 +12616,8 @@ function readBridgeConfig(config) {
|
|
|
12616
12616
|
const enabled = config.getOptionalBoolean("litellm.bridge.enabled") ?? false;
|
|
12617
12617
|
const issuer = config.getOptionalString("litellm.bridge.issuer");
|
|
12618
12618
|
const clientId = config.getOptionalString("litellm.bridge.clientId") ?? "abby-cli";
|
|
12619
|
-
|
|
12619
|
+
const allowedEmailDomains = (config.getOptionalStringArray("litellm.bridge.allowedEmailDomains") ?? []).map((d) => d.trim().toLowerCase()).filter(Boolean);
|
|
12620
|
+
return { enabled, issuer, clientId, allowedEmailDomains };
|
|
12620
12621
|
}
|
|
12621
12622
|
var BridgeConfigError = class extends Error {
|
|
12622
12623
|
};
|
|
@@ -12686,10 +12687,22 @@ function newDefaultVerifier(cfg) {
|
|
|
12686
12687
|
}
|
|
12687
12688
|
return new KeycloakJWTVerifier({ issuer: cfg.issuer, clientId: cfg.clientId });
|
|
12688
12689
|
}
|
|
12689
|
-
function
|
|
12690
|
-
|
|
12690
|
+
function normalizeIdentityOptions(opts) {
|
|
12691
|
+
const o = typeof opts === "string" ? { userIdDomain: opts } : opts ?? {};
|
|
12692
|
+
let domains = [];
|
|
12693
|
+
if (o.trustedEmailDomains?.length) {
|
|
12694
|
+
domains = o.trustedEmailDomains;
|
|
12695
|
+
} else if (o.userIdDomain) {
|
|
12696
|
+
domains = [o.userIdDomain];
|
|
12697
|
+
}
|
|
12698
|
+
const trusted = domains.map((d) => d.toLowerCase());
|
|
12699
|
+
return { userIdDomain: o.userIdDomain, trusted };
|
|
12700
|
+
}
|
|
12701
|
+
function resolveBridgeUserId(claims, identity) {
|
|
12702
|
+
const { userIdDomain, trusted } = normalizeIdentityOptions(identity);
|
|
12703
|
+
if (trusted.length === 0) {
|
|
12691
12704
|
throw new BridgeIdentityError(
|
|
12692
|
-
"litellm.
|
|
12705
|
+
"set litellm.bridge.allowedEmailDomains (or litellm.userIdDomain) to use the CLI bridge"
|
|
12693
12706
|
);
|
|
12694
12707
|
}
|
|
12695
12708
|
if (!claims.email) {
|
|
@@ -12702,13 +12715,14 @@ function resolveBridgeUserId(claims, userIdDomain) {
|
|
|
12702
12715
|
if (parts.length !== 2 || !parts[0]) {
|
|
12703
12716
|
throw new BridgeIdentityError("malformed email in token");
|
|
12704
12717
|
}
|
|
12705
|
-
const [
|
|
12706
|
-
if (
|
|
12718
|
+
const [emailLocalPart, domain] = parts;
|
|
12719
|
+
if (!trusted.includes(domain.toLowerCase())) {
|
|
12707
12720
|
throw new BridgeIdentityError(
|
|
12708
12721
|
`email domain mismatch: "${domain}" is not allowed for this deployment`
|
|
12709
12722
|
);
|
|
12710
12723
|
}
|
|
12711
|
-
|
|
12724
|
+
const name = claims.preferred_username?.trim() || emailLocalPart;
|
|
12725
|
+
return toLiteLLMUserId(name, userIdDomain);
|
|
12712
12726
|
}
|
|
12713
12727
|
async function getOrProvisionUserFromClaims(client, claims, provisioningEnabled, provisioningDefaults, logger, userIdDomain) {
|
|
12714
12728
|
const userId = resolveBridgeUserId(claims, userIdDomain);
|
|
@@ -12794,6 +12808,10 @@ function registerBridgeRoutes(router, ctx, bridgeOpts) {
|
|
|
12794
12808
|
const { config } = bridgeOpts;
|
|
12795
12809
|
const generateKeyInputSchema = (0, import_backstage_plugin_litellm_common7.createGenerateKeyInputSchema)(ctx.keyValidationConfig);
|
|
12796
12810
|
const bridgeCfg = readBridgeConfig(config);
|
|
12811
|
+
const bridgeIdentity = {
|
|
12812
|
+
userIdDomain,
|
|
12813
|
+
trustedEmailDomains: bridgeCfg.allowedEmailDomains
|
|
12814
|
+
};
|
|
12797
12815
|
let tokenVerifier = bridgeOpts.tokenVerifier;
|
|
12798
12816
|
if (bridgeCfg.enabled && !tokenVerifier) {
|
|
12799
12817
|
try {
|
|
@@ -12844,7 +12862,7 @@ function registerBridgeRoutes(router, ctx, bridgeOpts) {
|
|
|
12844
12862
|
provisioningEnabled,
|
|
12845
12863
|
provisioningDefaults,
|
|
12846
12864
|
logger,
|
|
12847
|
-
|
|
12865
|
+
bridgeIdentity
|
|
12848
12866
|
);
|
|
12849
12867
|
res.json(keys);
|
|
12850
12868
|
} catch (error) {
|
|
@@ -12864,7 +12882,7 @@ function registerBridgeRoutes(router, ctx, bridgeOpts) {
|
|
|
12864
12882
|
return;
|
|
12865
12883
|
}
|
|
12866
12884
|
const input = parseResult.data;
|
|
12867
|
-
const userId = resolveBridgeUserId(claims,
|
|
12885
|
+
const userId = resolveBridgeUserId(claims, bridgeIdentity);
|
|
12868
12886
|
const keyCreateCtx = {
|
|
12869
12887
|
client,
|
|
12870
12888
|
config: {
|