From a80b889760bc77eb6f6381ad1738df984729e30e Mon Sep 17 00:00:00 2001 From: gitlawr Date: Fri, 29 May 2026 16:50:53 +0800 Subject: [PATCH] refactor(access): fold allowed_users into allowed_principals Move the model-route access modal off the deprecated allowed_users policy/field onto the unified allowed_principals + principals surface, persisting everything through a single /access POST. - "specific users" radio now uses allowed_principals; a returned legacy allowed_users value is normalized so existing routes still select it. - derive the picker's selection and the full grant set from `principals` in GET /access (fall back to legacy `items` if a backend doesn't return principals yet). - save as `principals`: the principal-based override sends its staged set; the user picker maps its selection to USER-kind grants and preserves any non-user grants from the snapshot (no longer sends `users`). - guard saving before the GET seeds principals (would wipe grants); share the ALLOWED_PRINCIPALS_POLICY constant. - AccessControlFormData: `users` optional, add `principals`. --- src/pages/llmodels/apis/index.ts | 17 +++-- .../components/access-control-modal/form.tsx | 70 ++++++++++++++----- .../components/access-control-modal/index.tsx | 63 +++++++++++++---- src/pages/llmodels/config/types.ts | 19 ++++- 4 files changed, 132 insertions(+), 37 deletions(-) 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 (
onFinish(form.getFieldsValue(true))} preserve={true} clearOnDestroy={true} scrollToFirstError={true} @@ -396,7 +431,7 @@ const AccessControlForm = forwardRef((props: AccessControlFormProps, ref) => { label: intl.formatMessage({ id: 'models.accessSettings.allowedUsers' }), - value: 'allowed_users' + value: ALLOWED_PRINCIPALS_POLICY }, { label: intl.formatMessage({ @@ -424,15 +459,14 @@ const AccessControlForm = forwardRef((props: AccessControlFormProps, ref) => { action={action} /> )} - {allowedUsersOverride && - accessPolicy === overridePolicyValue && ( - - )} - {!allowedUsersOverride && accessPolicy === 'allowed_users' && ( + {allowedUsersOverride && accessPolicy === overridePolicyValue && ( + + )} + {!allowedUsersOverride && accessPolicy === ALLOWED_PRINCIPALS_POLICY && ( <>