mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-07 08:26:10 +00:00
fix(ui): withhold team-scoped model writes from view-only sessions too
The route-level RBAC in litellm/proxy/auth/route_checks.py 403s /model/new, /model/update, and /model/delete for proxy_admin_viewer on the session role alone, before ModelManagementAuthChecks' team-admin carve-out can run. A view-only session therefore gets no model write affordance, team admin or not.
This commit is contained in:
parent
74fb398f9b
commit
b6bd749c02
2 changed files with 26 additions and 16 deletions
|
|
@ -53,11 +53,11 @@ describe("modelCreationScope", () => {
|
|||
expect(canCreateModels(VIEW_ONLY_ADMIN, { teams: [], ...noLimits })).toBe(false);
|
||||
});
|
||||
|
||||
// A blunt view-only gate would fail this: team-admin membership legitimately grants
|
||||
// team-scoped creation, whatever the session role says.
|
||||
it("still requires a team from a view-only admin who admins a team", () => {
|
||||
// _check_proxy_admin_viewer_access (route_checks.py) 403s /model/new on the session role
|
||||
// alone, before the team-scoped carve-out in ModelManagementAuthChecks can run.
|
||||
it("forbids a view-only admin even when they admin a team", () => {
|
||||
expect(modelCreationScope(VIEW_ONLY_ADMIN, { teams: teamWhere("u-viewer", "admin"), ...noLimits })).toBe(
|
||||
"team-required",
|
||||
"forbidden",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -105,7 +105,9 @@ describe("canModifyModel", () => {
|
|||
expect(canModifyModel(VIEW_ONLY_ADMIN, null, teamRow)).toBe(false);
|
||||
});
|
||||
|
||||
it("lets a view-only user who admins the owning team act on its row", () => {
|
||||
expect(canModifyModel(VIEW_ONLY_ADMIN, teamWhere("u-viewer", "admin"), teamRow)).toBe(true);
|
||||
// The route RBAC blocks /model/update and /model/delete for the viewer role before the
|
||||
// team-scoped carve-out runs, so team-admin membership changes nothing here either.
|
||||
it("refuses a view-only user even when they admin the owning team", () => {
|
||||
expect(canModifyModel(VIEW_ONLY_ADMIN, teamWhere("u-viewer", "admin"), teamRow)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,14 +3,17 @@ import { Team } from "@/components/networking";
|
|||
import { isProxyAdminRole, isUserTeamAdminForAnyTeam, isUserTeamAdminForSingleTeam } from "./roles";
|
||||
|
||||
/**
|
||||
* The dashboard's mirror of ModelManagementAuthChecks in
|
||||
* litellm/proxy/management_endpoints/model_management_endpoints.py.
|
||||
* The dashboard's mirror of the two server layers that gate model writes: the role-level
|
||||
* route RBAC (`_check_proxy_admin_viewer_access` in litellm/proxy/auth/route_checks.py),
|
||||
* which 403s /model/new, /model/update, and /model/delete for every view-only session
|
||||
* before the endpoint runs, and ModelManagementAuthChecks in
|
||||
* litellm/proxy/management_endpoints/model_management_endpoints.py behind it.
|
||||
*
|
||||
* Both questions below are answered there by exactly two inputs: the caller's role, and
|
||||
* whether the caller admins the team named in `model_info.team_id`. `created_by` is written
|
||||
* at creation and never read by an auth check, so it is deliberately absent here; gating on
|
||||
* it hid controls from team admins the API accepts, and showed controls to former team admins
|
||||
* the API rejects.
|
||||
* Past that route gate, both questions below are answered by exactly two inputs: the
|
||||
* caller's role, and whether the caller admins the team named in `model_info.team_id`.
|
||||
* `created_by` is written at creation and never read by an auth check, so it is deliberately
|
||||
* absent here; gating on it hid controls from team admins the API accepts, and showed
|
||||
* controls to former team admins the API rejects.
|
||||
*/
|
||||
export interface ModelActor {
|
||||
userRole: string | null;
|
||||
|
|
@ -42,13 +45,18 @@ const isTeamAdminOf = (teams: Team[] | null, userID: string, teamId: string): bo
|
|||
|
||||
/**
|
||||
* POST /model/new takes a proxy admin unconditionally, or a team admin whose payload names a
|
||||
* team; an unscoped create from anyone else is a 403. Returning the requirement rather than a
|
||||
* pair of booleans keeps "may not create" and "may create unscoped" from being confused.
|
||||
* team; an unscoped create from anyone else is a 403. A view-only session is 403d by the
|
||||
* route RBAC on its role alone, so team-admin membership cannot rescue it. Returning the
|
||||
* requirement rather than a pair of booleans keeps "may not create" and "may create
|
||||
* unscoped" from being confused.
|
||||
*/
|
||||
export const modelCreationScope = (
|
||||
actor: ModelActor,
|
||||
{ teams, disabledForInternalUsers }: ModelCreationLimits,
|
||||
): ModelWriteScope => {
|
||||
if (actor.isViewOnly) {
|
||||
return "forbidden";
|
||||
}
|
||||
if (isWritableProxyAdmin(actor)) {
|
||||
return "unscoped-ok";
|
||||
}
|
||||
|
|
@ -76,7 +84,7 @@ export const canModifyModel = (
|
|||
teams: Team[] | null,
|
||||
{ teamId, isDbModel }: ModelRowOrigin,
|
||||
): boolean => {
|
||||
if (!isDbModel) {
|
||||
if (actor.isViewOnly || !isDbModel) {
|
||||
return false;
|
||||
}
|
||||
if (isWritableProxyAdmin(actor)) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue