From 099b419e34a4f07d09c433d2593fcec2d900f60f Mon Sep 17 00:00:00 2001 From: gitlawr Date: Wed, 27 May 2026 12:21:15 +0800 Subject: [PATCH] fix: source owner_principal_id from current organization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OwnerPrincipalIdField was reading currentClusterAtom, which no callers ever wrote — so every form submission ended up with owner_principal_id=null and the backend rejected non-admins with "Only platform admin can create global ...". Pin the field to currentOrganizationId instead (the Org the caller is acting under). Cluster ownership is the wrong source: a cluster_access grant lets one Org schedule on another Org's cluster, but the new resource is still owned by the caller's Org, and the backend enforces owner_principal_id == ctx.current_principal_id. Also drops the dead currentClusterAtom. --- src/atoms/gpuservice.ts | 6 ------ src/atoms/user.ts | 9 +++++++++ .../_components/owner-principal-id-field.tsx | 16 ++++++++++------ 3 files changed, 19 insertions(+), 12 deletions(-) delete mode 100644 src/atoms/gpuservice.ts diff --git a/src/atoms/gpuservice.ts b/src/atoms/gpuservice.ts deleted file mode 100644 index f7f9df55..00000000 --- a/src/atoms/gpuservice.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { ClusterListItem } from '@/pages/cluster-management/config/types'; -import { atom } from 'jotai'; - -export const currentClusterAtom = atom< - (Partial & { label?: string; value?: number }) | null ->(null); diff --git a/src/atoms/user.ts b/src/atoms/user.ts index ecfa1134..7826aa2d 100644 --- a/src/atoms/user.ts +++ b/src/atoms/user.ts @@ -3,6 +3,15 @@ import { atomWithStorage } from 'jotai/utils'; export const userAtom = atomWithStorage('userInfo', null); +// Backs the `currentOrganizationId` localStorage key. Stays null in +// builds with no Org context (single-tenant), and is shared with any +// extension that persists the same key so both sides stay in sync +// without one side having to import from the other. +export const currentOrganizationIdAtom = atomWithStorage( + 'currentOrganizationId', + null +); + export const GPUStackVersionAtom = atom<{ version: string; git_commit: string; diff --git a/src/pages/_components/owner-principal-id-field.tsx b/src/pages/_components/owner-principal-id-field.tsx index 06919db6..4b6d0562 100644 --- a/src/pages/_components/owner-principal-id-field.tsx +++ b/src/pages/_components/owner-principal-id-field.tsx @@ -1,4 +1,4 @@ -import { currentClusterAtom } from '@/atoms/gpuservice'; +import { currentOrganizationIdAtom } from '@/atoms/user'; import { Input as CInput } from '@gpustack/core-ui'; import { Form } from 'antd'; import type { NamePath } from 'antd/es/form/interface'; @@ -9,18 +9,22 @@ interface OwnerPrincipalIdFieldProps { name?: NamePath; } +// Pins `owner_principal_id` to the Org the caller is currently acting +// under. Cluster ownership is irrelevant here: cluster_access grants +// let one Org schedule on another Org's cluster, but the resource the +// caller creates still belongs to *their* Org, and the backend enforces +// `owner_principal_id == ctx.current_principal_id`. const OwnerPrincipalIdField: React.FC = ({ name = 'owner_principal_id' }) => { - const currentCluster = useAtomValue(currentClusterAtom); - const ownerPrincipalId = currentCluster?.owner_principal_id; + const currentOrgId = useAtomValue(currentOrganizationIdAtom); const form = Form.useFormInstance(); useEffect(() => { - if (ownerPrincipalId != null) { - form.setFieldValue(name, ownerPrincipalId); + if (currentOrgId != null) { + form.setFieldValue(name, currentOrgId); } - }, [ownerPrincipalId, name, form]); + }, [currentOrgId, name, form]); return (