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:
@@ -2,6 +2,7 @@ import { PageAction } from '@/config';
|
|||||||
import { PageActionType } from '@/config/types';
|
import { PageActionType } from '@/config/types';
|
||||||
import { RouteItem } from '@/pages/model-routes/config/types';
|
import { RouteItem } from '@/pages/model-routes/config/types';
|
||||||
import { queryUserDirectory } from '@/pages/users/apis';
|
import { queryUserDirectory } from '@/pages/users/apis';
|
||||||
|
import { getGPUStackPlugin } from '@/plugins';
|
||||||
import { DownOutlined, QuestionCircleOutlined } from '@ant-design/icons';
|
import { DownOutlined, QuestionCircleOutlined } from '@ant-design/icons';
|
||||||
import {
|
import {
|
||||||
AlertBlockInfo,
|
AlertBlockInfo,
|
||||||
@@ -33,7 +34,7 @@ import { AccessControlFormData } from '../../config/types';
|
|||||||
|
|
||||||
type TransferKey = string | number | bigint;
|
type TransferKey = string | number | bigint;
|
||||||
|
|
||||||
const accessScopeTips = [
|
const buildAccessScopeTips = (override?: AllowedUsersOverride) => [
|
||||||
{
|
{
|
||||||
title: {
|
title: {
|
||||||
text: 'models.accessSettings.authed',
|
text: 'models.accessSettings.authed',
|
||||||
@@ -43,10 +44,10 @@ const accessScopeTips = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: {
|
title: {
|
||||||
text: 'models.accessSettings.allowedUsers',
|
text: override?.labelId ?? 'models.accessSettings.allowedUsers',
|
||||||
locale: true
|
locale: true
|
||||||
},
|
},
|
||||||
tips: 'models.accessSettings.allowedUsers.tips'
|
tips: override?.tipsId ?? 'models.accessSettings.allowedUsers.tips'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: {
|
title: {
|
||||||
@@ -74,11 +75,31 @@ interface AccessControlFormProps {
|
|||||||
onValuesChange?: (changedValues: any, allValues: any) => void;
|
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 AccessControlForm = forwardRef((props: AccessControlFormProps, ref) => {
|
||||||
const { action, currentData, onFinish, onValuesChange } = props;
|
const { action, currentData, onFinish, onValuesChange } = props;
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const accessPolicy = Form.useWatch('access_policy', form);
|
const accessPolicy = Form.useWatch('access_policy', form);
|
||||||
|
const allowedUsersOverride: AllowedUsersOverride | undefined =
|
||||||
|
getGPUStackPlugin()?.accessControl?.allowedUsersOverride;
|
||||||
|
const overridePolicyValue = allowedUsersOverride?.policyValue;
|
||||||
const [targetKeys, setTargetKeys] = useState<TransferKey[]>([]);
|
const [targetKeys, setTargetKeys] = useState<TransferKey[]>([]);
|
||||||
const [totalPages, setTotalPages] = useState(0);
|
const [totalPages, setTotalPages] = useState(0);
|
||||||
const [userList, setUserList] = useState<
|
const [userList, setUserList] = useState<
|
||||||
@@ -296,7 +317,13 @@ const AccessControlForm = forwardRef((props: AccessControlFormProps, ref) => {
|
|||||||
>
|
>
|
||||||
<Label>
|
<Label>
|
||||||
{intl.formatMessage({ id: 'models.table.accessScope' })}
|
{intl.formatMessage({ id: 'models.table.accessScope' })}
|
||||||
<Tooltip title={<TooltipList list={accessScopeTips}></TooltipList>}>
|
<Tooltip
|
||||||
|
title={
|
||||||
|
<TooltipList
|
||||||
|
list={buildAccessScopeTips(allowedUsersOverride)}
|
||||||
|
></TooltipList>
|
||||||
|
}
|
||||||
|
>
|
||||||
<QuestionCircleOutlined />
|
<QuestionCircleOutlined />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Label>
|
</Label>
|
||||||
@@ -310,12 +337,19 @@ const AccessControlForm = forwardRef((props: AccessControlFormProps, ref) => {
|
|||||||
label: intl.formatMessage({ id: 'models.accessSettings.authed' }),
|
label: intl.formatMessage({ id: 'models.accessSettings.authed' }),
|
||||||
value: 'authed'
|
value: 'authed'
|
||||||
},
|
},
|
||||||
{
|
allowedUsersOverride
|
||||||
label: intl.formatMessage({
|
? {
|
||||||
id: 'models.accessSettings.allowedUsers'
|
label: intl.formatMessage({
|
||||||
}),
|
id: allowedUsersOverride.labelId
|
||||||
value: 'allowed_users'
|
}),
|
||||||
},
|
value: allowedUsersOverride.policyValue
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
label: intl.formatMessage({
|
||||||
|
id: 'models.accessSettings.allowedUsers'
|
||||||
|
}),
|
||||||
|
value: 'allowed_users'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: intl.formatMessage({
|
label: intl.formatMessage({
|
||||||
id: 'models.accessSettings.public'
|
id: 'models.accessSettings.public'
|
||||||
@@ -335,7 +369,15 @@ const AccessControlForm = forwardRef((props: AccessControlFormProps, ref) => {
|
|||||||
></AlertBlockInfo>
|
></AlertBlockInfo>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{accessPolicy === 'allowed_users' && (
|
{allowedUsersOverride &&
|
||||||
|
accessPolicy === overridePolicyValue && (
|
||||||
|
<allowedUsersOverride.Field
|
||||||
|
form={form}
|
||||||
|
routeId={currentData?.id}
|
||||||
|
action={action}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!allowedUsersOverride && accessPolicy === 'allowed_users' && (
|
||||||
<>
|
<>
|
||||||
<Label>
|
<Label>
|
||||||
{intl.formatMessage({ id: 'models.table.userSelection' })}
|
{intl.formatMessage({ id: 'models.table.userSelection' })}
|
||||||
|
|||||||
@@ -23,8 +23,14 @@ const AccessControlModal: React.FC<
|
|||||||
|
|
||||||
const handleOnFinish = async (values: AccessControlFormData) => {
|
const handleOnFinish = async (values: AccessControlFormData) => {
|
||||||
try {
|
try {
|
||||||
const data = {
|
const data: any = {
|
||||||
access_policy: values.access_policy,
|
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:
|
users:
|
||||||
values.access_policy === 'allowed_users' ? values.users || [] : []
|
values.access_policy === 'allowed_users' ? values.users || [] : []
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user