diff --git a/src/pages/llmodels/apis/index.ts b/src/pages/llmodels/apis/index.ts index 7e388dff..c44e1c42 100644 --- a/src/pages/llmodels/apis/index.ts +++ b/src/pages/llmodels/apis/index.ts @@ -445,11 +445,18 @@ export async function queryModelAccessUserList(id: number) { // The response carries `access_policy` alongside `items` so the // Access Settings dialog can refresh both halves from a single // GET (the calling list snapshot may be stale after a prior - // save). - return request<{ items: UserListItem[]; access_policy?: string }>( - `${MODEL_ROUTES}/${id}/access`, - { method: 'GET' } - ); + // save). `principals` is the full grant set (any kind) used by the + // principal-based override; `items` stays the USER-only subset. + return request<{ + items: UserListItem[]; + access_policy?: string; + principals?: { + principal_type: string; + principal_id: number; + principal_name?: string; + principal_display_name?: string; + }[]; + }>(`${MODEL_ROUTES}/${id}/access`, { method: 'GET' }); } export async function updateModelAccessUser(params: { diff --git a/src/pages/llmodels/components/access-control-modal/form.tsx b/src/pages/llmodels/components/access-control-modal/form.tsx index 756863d2..d42ecfa8 100644 --- a/src/pages/llmodels/components/access-control-modal/form.tsx +++ b/src/pages/llmodels/components/access-control-modal/form.tsx @@ -34,6 +34,16 @@ import { AccessControlFormData } from '../../config/types'; type TransferKey = string | number | bigint; +// The "specific users" policy is now ALLOWED_PRINCIPALS with a +// user-only grant list — the same value the principal-based override +// (when a plugin provides one) uses, so the two interoperate. +// `allowed_users` is the deprecated value released in v2.1.x; normalize +// it so legacy routes still select the "specific users" radio (they +// converge to ALLOWED_PRINCIPALS on save). +export const ALLOWED_PRINCIPALS_POLICY = 'allowed_principals'; +const normalizeAccessPolicy = (p?: string) => + p === 'allowed_users' ? ALLOWED_PRINCIPALS_POLICY : p; + const buildAccessScopeTips = ( override?: AllowedUsersOverride, prepended: PrependedPolicy[] = [] @@ -216,7 +226,7 @@ const AccessControlForm = forwardRef((props: AccessControlFormProps, ref) => { const handleOnPolicyChange = async (e: RadioChangeEvent) => { console.log('policy changed:', e.target.value); const policy = e.target.value; - if (policy === 'allowed_users') { + if (policy === ALLOWED_PRINCIPALS_POLICY) { form.setFieldsValue({ users: formDataCacheRef.current?.users || [] }); } else { formDataCacheRef.current = { @@ -256,16 +266,31 @@ const AccessControlForm = forwardRef((props: AccessControlFormProps, ref) => { // server's authoritative value, which is what survives a save // when the parent list hasn't been refreshed. form.setFieldsValue({ - access_policy: currentData?.access_policy + access_policy: normalizeAccessPolicy(currentData?.access_policy) }); queryModelAccessUserList(currentData.id).then((res) => { - const keys = res.items.map((item) => item.id); - setTargetKeys(keys); + // Fall back to the legacy `items` (USER-only) field when an + // older backend doesn't return `principals` yet. + const principals = + res.principals ?? + res.items?.map((item) => ({ + principal_type: 'user', + principal_id: item.id + })) ?? + []; + // Derive the user picker's selection from the unified + // `principals` set (USER-kind subset), not the deprecated + // `items` field. `principals` is also kept whole so a save can + // preserve any non-user grants it doesn't manage. + const userKeys = principals + .filter((p) => p.principal_type === 'user') + .map((p) => p.principal_id); + setTargetKeys(userKeys); let hasAdmin = false; let hasInactive = false; - for (const key of keys) { + for (const key of userKeys) { const user = userMap.get(key); if (!user) continue; if (user.is_admin) hasAdmin = true; @@ -280,8 +305,14 @@ const AccessControlForm = forwardRef((props: AccessControlFormProps, ref) => { setFilterInUsers(filterSet); form.setFieldsValue({ - access_policy: res.access_policy ?? currentData.access_policy, - users: res.items.map((item) => ({ id: item.id })) + access_policy: normalizeAccessPolicy( + res.access_policy ?? currentData.access_policy + ), + users: userKeys.map((id) => ({ id })), + // Keep the full grant set: read by the principal-based + // override Field, and used on save to preserve non-user + // grants when the user picker submits `principals`. + principals }); }); } else { @@ -340,7 +371,11 @@ const AccessControlForm = forwardRef((props: AccessControlFormProps, ref) => { return (