feat: access-control modal seam to swap the allowed_users surface

The OSS Access Settings form hardcodes the `allowed_users` policy
(per-user explicit grants via the legacy transfer widget). Multi-
tenancy needs a different surface that grants by Org / Group / User
principals via the `/v2/model-routes/{id}/principals` endpoints, and
the enterprise plugin will ship that — but the OSS form should stay
visually unchanged when no plugin is loaded.

Add a plugin extension point on the form: if a registered plugin
exposes `accessControl.allowedUsersOverride: { policyValue, labelId,
tipsId?, Field }`, the form replaces the `allowed_users` radio entry
with the plugin's labelled option, swaps the access-scope tooltip
copy, and renders `<Field form routeId action />` for the override
policy. Without a plugin the form's radio / tooltip / content path
are unchanged.

`handleOnFinish` continues to send `users` only for `allowed_users`;
any other policy (including the plugin's value) goes through with
an empty users list, leaving the plugin's Field to manage its own
principal CRUD inline.
This commit is contained in:
gitlawr
2026-05-11 19:30:21 +08:00
committed by jialin
parent 93fa277bdd
commit f37fad11b1
2 changed files with 60 additions and 12 deletions
@@ -2,6 +2,7 @@ import { PageAction } from '@/config';
import { PageActionType } from '@/config/types';
import { RouteItem } from '@/pages/model-routes/config/types';
import { queryUserDirectory } from '@/pages/users/apis';
import { getGPUStackPlugin } from '@/plugins';
import { DownOutlined, QuestionCircleOutlined } from '@ant-design/icons';
import {
AlertBlockInfo,
@@ -33,7 +34,7 @@ import { AccessControlFormData } from '../../config/types';
type TransferKey = string | number | bigint;
const accessScopeTips = [
const buildAccessScopeTips = (override?: AllowedUsersOverride) => [
{
title: {
text: 'models.accessSettings.authed',
@@ -43,10 +44,10 @@ const accessScopeTips = [
},
{
title: {
text: 'models.accessSettings.allowedUsers',
text: override?.labelId ?? 'models.accessSettings.allowedUsers',
locale: true
},
tips: 'models.accessSettings.allowedUsers.tips'
tips: override?.tipsId ?? 'models.accessSettings.allowedUsers.tips'
},
{
title: {
@@ -74,11 +75,31 @@ interface AccessControlFormProps {
onValuesChange?: (changedValues: any, allValues: any) => void;
}
// Extension seam: an enterprise plugin can substitute the legacy
// `allowed_users` policy entry with a different one (e.g. the
// principal-based ALLOWED_PRINCIPALS surface). When the override is
// present, the OSS `allowed_users` radio option is swapped out and
// the override's ``Field`` component is rendered when that policy is
// active. Without a plugin the OSS form is unchanged.
type AllowedUsersOverride = {
policyValue: string;
labelId: string;
tipsId?: string;
Field: React.ComponentType<{
form: any;
routeId?: number;
action: PageActionType;
}>;
};
const AccessControlForm = forwardRef((props: AccessControlFormProps, ref) => {
const { action, currentData, onFinish, onValuesChange } = props;
const intl = useIntl();
const [form] = Form.useForm();
const accessPolicy = Form.useWatch('access_policy', form);
const allowedUsersOverride: AllowedUsersOverride | undefined =
getGPUStackPlugin()?.accessControl?.allowedUsersOverride;
const overridePolicyValue = allowedUsersOverride?.policyValue;
const [targetKeys, setTargetKeys] = useState<TransferKey[]>([]);
const [totalPages, setTotalPages] = useState(0);
const [userList, setUserList] = useState<
@@ -296,7 +317,13 @@ const AccessControlForm = forwardRef((props: AccessControlFormProps, ref) => {
>
<Label>
{intl.formatMessage({ id: 'models.table.accessScope' })}
<Tooltip title={<TooltipList list={accessScopeTips}></TooltipList>}>
<Tooltip
title={
<TooltipList
list={buildAccessScopeTips(allowedUsersOverride)}
></TooltipList>
}
>
<QuestionCircleOutlined />
</Tooltip>
</Label>
@@ -310,12 +337,19 @@ const AccessControlForm = forwardRef((props: AccessControlFormProps, ref) => {
label: intl.formatMessage({ id: 'models.accessSettings.authed' }),
value: 'authed'
},
{
label: intl.formatMessage({
id: 'models.accessSettings.allowedUsers'
}),
value: 'allowed_users'
},
allowedUsersOverride
? {
label: intl.formatMessage({
id: allowedUsersOverride.labelId
}),
value: allowedUsersOverride.policyValue
}
: {
label: intl.formatMessage({
id: 'models.accessSettings.allowedUsers'
}),
value: 'allowed_users'
},
{
label: intl.formatMessage({
id: 'models.accessSettings.public'
@@ -335,7 +369,15 @@ const AccessControlForm = forwardRef((props: AccessControlFormProps, ref) => {
></AlertBlockInfo>
</div>
)}
{accessPolicy === 'allowed_users' && (
{allowedUsersOverride &&
accessPolicy === overridePolicyValue && (
<allowedUsersOverride.Field
form={form}
routeId={currentData?.id}
action={action}
/>
)}
{!allowedUsersOverride && accessPolicy === 'allowed_users' && (
<>
<Label>
{intl.formatMessage({ id: 'models.table.userSelection' })}
@@ -23,8 +23,14 @@ const AccessControlModal: React.FC<
const handleOnFinish = async (values: AccessControlFormData) => {
try {
const data = {
const data: any = {
access_policy: values.access_policy,
// `users` is only meaningful for the legacy `allowed_users`
// policy; for the plugin override (typically the principal-
// based policy) the plugin's Field manages its own state
// inline via the principal CRUD endpoints, so we send an
// empty list to clear any stale user grants from a prior
// policy switch.
users:
values.access_policy === 'allowed_users' ? values.users || [] : []
};