@acarmisc/backstage-plugin-litellm-backend 0.27.0 → 0.28.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.
@@ -12,6 +12,23 @@ const withUser_1 = require("./middleware/withUser");
12
12
  function registerTeamUsageRoutes(router, ctx) {
13
13
  const { client, catalogClient, auth, logger, teamBudgetVisibility, } = ctx;
14
14
  const requireUser = (0, withUser_1.createRequireUser)(ctx);
15
+ // A team manager: a member of the team's owning group (team management
16
+ // enabled), or a member of the catalog lead group `<alias><leadGroupSuffix>`
17
+ // for teams synced without an owning_group.
18
+ async function isTeamManager(team, tokenEntityRef) {
19
+ const owningGroup = typeof team.metadata?.owning_group === 'string' ? team.metadata.owning_group : undefined;
20
+ if (ctx.teamMgmtEnabled &&
21
+ owningGroup &&
22
+ (await (0, provisioning_1.isUserMemberOfGroup)(tokenEntityRef, owningGroup, catalogClient, auth, logger))) {
23
+ return true;
24
+ }
25
+ const suffix = ctx.memberBreakdownCfg.leadGroupSuffix;
26
+ if (!suffix || !team.team_alias)
27
+ return false;
28
+ // Same slug rule as the catalog group names: lower-case, other chars become '-'.
29
+ const slug = team.team_alias.toLowerCase().replace(/[^a-z0-9\-_.]/g, '-');
30
+ return (0, provisioning_1.isUserMemberOfGroup)(tokenEntityRef, `group:default/${slug}${suffix}`, catalogClient, auth, logger);
31
+ }
15
32
  router.get('/teams/:teamId/usage', requireUser, async (req, res) => {
16
33
  try {
17
34
  const { teamId } = req.params;
@@ -25,24 +42,20 @@ function registerTeamUsageRoutes(router, ctx) {
25
42
  const userInfo = await (0, withUser_1.getProvisionedUser)(ctx, res);
26
43
  // Check membership: user must be in the team OR be a team manager
27
44
  const isMember = userInfo?.teams?.includes(teamId) ?? false;
28
- // A "manager" administers this specific team: team management is enabled
29
- // and the caller belongs to the team's owning group (metadata.owning_group).
45
+ // A "manager" administers this specific team (see isTeamManager).
30
46
  // Membership of the global team-admin group alone is not enough.
31
47
  let isManager = false;
32
48
  // Skip the extra lookup when it can't change the outcome: members always
33
49
  // get through, and manager-ness only matters for members when the budget
34
50
  // hiding flags differ between the two roles.
35
- const managerCheckNeeded = ctx.teamMgmtEnabled &&
51
+ const managerCheckNeeded = (ctx.teamMgmtEnabled || !!ctx.memberBreakdownCfg.leadGroupSuffix) &&
36
52
  (!isMember ||
37
53
  teamBudgetVisibility.hideTeamBudgetForMembers !==
38
54
  teamBudgetVisibility.hideTeamBudgetForManagers);
39
55
  if (managerCheckNeeded) {
40
56
  try {
41
57
  const team = await (0, teamFetch_1.withTeamFetchRetry)(() => client.getTeamInfo(teamId));
42
- const owningGroup = typeof team.metadata?.owning_group === 'string' ? team.metadata.owning_group : undefined;
43
- if (owningGroup) {
44
- isManager = await (0, provisioning_1.isUserMemberOfGroup)(tokenEntityRef, owningGroup, catalogClient, auth, logger);
45
- }
58
+ isManager = await isTeamManager(team, tokenEntityRef);
46
59
  }
47
60
  catch (err) {
48
61
  const message = err instanceof Error ? err.message : String(err);
@@ -104,10 +117,7 @@ function registerTeamUsageRoutes(router, ctx) {
104
117
  }
105
118
  throw err;
106
119
  }
107
- const owningGroup = typeof team.metadata?.owning_group === 'string' ? team.metadata.owning_group : undefined;
108
- const isManager = ctx.teamMgmtEnabled &&
109
- !!owningGroup &&
110
- (await (0, provisioning_1.isUserMemberOfGroup)(tokenEntityRef, owningGroup, catalogClient, auth, logger));
120
+ const isManager = await isTeamManager(team, tokenEntityRef);
111
121
  if (!isMember && !isManager) {
112
122
  res.status(404).json({ error: 'Team not found' });
113
123
  return null;
@@ -8,6 +8,8 @@ export interface MemberBreakdownConfig {
8
8
  viewerRoles: string[];
9
9
  /** Whether `litellm.team.usage.read` may grant access (needs `permission.enabled`). */
10
10
  permissionEnabled: boolean;
11
+ /** Suffix of a catalog group named `<team_alias><suffix>` whose members are team leads. */
12
+ leadGroupSuffix?: string;
11
13
  }
12
14
  export declare function readMemberBreakdownConfig(config: Config): MemberBreakdownConfig;
13
15
  /** Catalog profile of a member, keyed by lower-cased email. */
@@ -11,6 +11,7 @@ function readMemberBreakdownConfig(config) {
11
11
  enabled: config.getOptionalBoolean('litellm.teamUsage.memberBreakdown.enabled') ?? false,
12
12
  viewerRoles: config.getOptionalStringArray('litellm.teamUsage.memberBreakdown.viewerRoles') ?? [],
13
13
  permissionEnabled: config.getOptionalBoolean('permission.enabled') ?? false,
14
+ leadGroupSuffix: config.getOptionalString('litellm.teamUsage.memberBreakdown.leadGroupSuffix') || undefined,
14
15
  };
15
16
  }
16
17
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acarmisc/backstage-plugin-litellm-backend",
3
- "version": "0.27.0",
3
+ "version": "0.28.0",
4
4
  "description": "The Backstage backend plugin for LiteLLM governance",
5
5
  "backstage": {
6
6
  "role": "backend-plugin",