diff --git a/web/app/components/datasets/settings/permission-selector/__tests__/index.spec.tsx b/web/app/components/datasets/settings/permission-selector/__tests__/index.spec.tsx index 1e07bebdd3..926f00fbb7 100644 --- a/web/app/components/datasets/settings/permission-selector/__tests__/index.spec.tsx +++ b/web/app/components/datasets/settings/permission-selector/__tests__/index.spec.tsx @@ -2,23 +2,8 @@ import type { Member } from '@/models/common' import { fireEvent, screen, waitFor } from '@testing-library/react' import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features' import { DatasetPermission } from '@/models/datasets' -import { LicenseStatus } from '@/types/feature' import PermissionSelector from '../index' -const mockConfig = vi.hoisted(() => ({ - IS_CLOUD_EDITION: false, -})) - -vi.mock('@/config', async (importOriginal) => { - const actual = await importOriginal() - return { - ...actual, - get IS_CLOUD_EDITION() { - return mockConfig.IS_CLOUD_EDITION - }, - } -}) - // Mock app-context vi.mock('@/context/app-context', () => ({ useSelector: () => ({ @@ -48,7 +33,6 @@ describe('PermissionSelector', () => { beforeEach(() => { vi.clearAllMocks() - mockConfig.IS_CLOUD_EDITION = false }) describe('Rendering', () => { @@ -424,23 +408,10 @@ describe('PermissionSelector', () => { expect(triggerElement)!.toBeInTheDocument() }) - it('should show access config hint and remain closed in SaaS', () => { - mockConfig.IS_CLOUD_EDITION = true - renderWithSystemFeatures() - - const trigger = screen.getByText(/form\.permissionsAccessConfig/) - fireEvent.click(trigger) - - expect(screen.getByText(/form\.permissionsAccessConfig/))!.toBeInTheDocument() - expect(screen.queryByText(/form\.permissionsOnlyMe/))!.not.toBeInTheDocument() - }) - - it('should show access config hint and remain closed in enterprise edition', () => { + it('should show access config hint and remain closed when RBAC is enabled', () => { renderWithSystemFeatures(, { systemFeatures: { - license: { - status: LicenseStatus.ACTIVE, - }, + rbac_enabled: true, }, }) diff --git a/web/app/components/datasets/settings/permission-selector/index.tsx b/web/app/components/datasets/settings/permission-selector/index.tsx index 98842ff3c8..51b6e87a58 100644 --- a/web/app/components/datasets/settings/permission-selector/index.tsx +++ b/web/app/components/datasets/settings/permission-selector/index.tsx @@ -11,11 +11,9 @@ import { useSuspenseQuery } from '@tanstack/react-query' import { useDebounceFn } from 'ahooks' import { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' -import { IS_CLOUD_EDITION } from '@/config' import { useSelector as useAppContextWithSelector } from '@/context/app-context' import { DatasetPermission } from '@/models/datasets' import { systemFeaturesQueryOptions } from '@/service/system-features' -import { LicenseStatus } from '@/types/feature' import MemberItem from './member-item' import Item from './permission-item' @@ -91,9 +89,8 @@ const PermissionSelector = ({ const isAllTeamMembers = permission === DatasetPermission.allTeamMembers const isPartialMembers = permission === DatasetPermission.partialMembers const selectedMemberNames = selectedMembers.map(member => member.name).join(', ') - const isEnterpriseEdition = systemFeatures.license.status !== LicenseStatus.NONE - const isEditionDisabled = IS_CLOUD_EDITION || isEnterpriseEdition - const isDisabled = disabled || isEditionDisabled + const isDisabledByRBAC = systemFeatures.rbac_enabled + const isDisabled = disabled || isDisabledByRBAC return ( - {isEditionDisabled && ( + {isDisabledByRBAC && ( <>
@@ -119,7 +116,7 @@ const PermissionSelector = ({ )} { - !isEditionDisabled && isOnlyMe && ( + !isDisabledByRBAC && isOnlyMe && ( <>
@@ -131,7 +128,7 @@ const PermissionSelector = ({ ) } { - !isEditionDisabled && isAllTeamMembers && ( + !isDisabledByRBAC && isAllTeamMembers && ( <>
@@ -143,7 +140,7 @@ const PermissionSelector = ({ ) } { - !isEditionDisabled && isPartialMembers && ( + !isDisabledByRBAC && isPartialMembers && ( <>
{ diff --git a/web/app/components/plugins/plugin-page/__tests__/index.spec.tsx b/web/app/components/plugins/plugin-page/__tests__/index.spec.tsx index 0be87e798d..04eaaa99dc 100644 --- a/web/app/components/plugins/plugin-page/__tests__/index.spec.tsx +++ b/web/app/components/plugins/plugin-page/__tests__/index.spec.tsx @@ -42,7 +42,7 @@ vi.mock('@/context/app-context', () => ({ isCurrentWorkspaceManager: true, isCurrentWorkspaceOwner: false, langGeniusVersionInfo: { current_version: '1.0.0' }, - workspacePermissionKeys: ['plugin.install', 'plugin.manage', 'plugin.preference.manage'], + workspacePermissionKeys: ['plugin.install', 'plugin.manage', 'plugin.debug'], }), })) diff --git a/web/app/components/plugins/plugin-page/__tests__/use-reference-setting.spec.ts b/web/app/components/plugins/plugin-page/__tests__/use-reference-setting.spec.ts index 3f713bcad9..365c5325d7 100644 --- a/web/app/components/plugins/plugin-page/__tests__/use-reference-setting.spec.ts +++ b/web/app/components/plugins/plugin-page/__tests__/use-reference-setting.spec.ts @@ -25,7 +25,7 @@ const createAppContext = (overrides: Partial> = isCurrentWorkspaceManager: false, isCurrentWorkspaceOwner: false, langGeniusVersionInfo: { current_version: '1.0.0' }, - workspacePermissionKeys: ['plugin.install', 'plugin.manage'], + workspacePermissionKeys: ['plugin.install', 'plugin.manage', 'plugin.debug'], ...overrides, }) as ReturnType @@ -54,115 +54,49 @@ describe('useReferenceSetting Hook', () => { vi.mocked(useInvalidateReferenceSettings).mockReturnValue(vi.fn()) }) - describe('hasPermission logic', () => { - it('should return false when permission is undefined', () => { - vi.mocked(useReferenceSettings).mockReturnValue({ - data: { - permission: { - install_permission: undefined, - debug_permission: undefined, - }, - }, - } as unknown as ReturnType) + describe('workspace permission key logic', () => { + it('should return false when workspace permission keys are empty', () => { + vi.mocked(useAppContext).mockReturnValue(createAppContext({ + workspacePermissionKeys: [], + })) const { result } = renderHook(() => useReferenceSetting()) expect(result.current.canInstall).toBe(false) + expect(result.current.canUpdate).toBe(false) + expect(result.current.canViewInstalledPlugins).toBe(false) + expect(result.current.canManagePlugin).toBe(false) + expect(result.current.canUninstall).toBe(false) expect(result.current.canDebugger).toBe(false) }) - it('should return false when permission is noOne', () => { - vi.mocked(useReferenceSettings).mockReturnValue({ - data: { - permission: { - install_permission: PermissionType.noOne, - debug_permission: PermissionType.noOne, - }, - }, - } as ReturnType) + it('should only allow debug with plugin.debug only', () => { + vi.mocked(useAppContext).mockReturnValue(createAppContext({ + workspacePermissionKeys: ['plugin.debug'], + })) const { result } = renderHook(() => useReferenceSetting()) expect(result.current.canInstall).toBe(false) - expect(result.current.canDebugger).toBe(false) + expect(result.current.canUpdate).toBe(false) + expect(result.current.canViewInstalledPlugins).toBe(false) + expect(result.current.canManagePlugin).toBe(false) + expect(result.current.canUninstall).toBe(false) + expect(result.current.canDebugger).toBe(true) }) - it('should return true when permission is everyone', () => { - vi.mocked(useReferenceSettings).mockReturnValue({ - data: { - permission: { - install_permission: PermissionType.everyone, - debug_permission: PermissionType.everyone, - }, - }, - } as ReturnType) + it('should allow install and update but not manage or debug with plugin.install only', () => { + vi.mocked(useAppContext).mockReturnValue(createAppContext({ + workspacePermissionKeys: ['plugin.install'], + })) const { result } = renderHook(() => useReferenceSetting()) expect(result.current.canInstall).toBe(true) - expect(result.current.canDebugger).toBe(true) - }) - - it('should return isAdmin when permission is admin and user is manager', () => { - vi.mocked(useAppContext).mockReturnValue(createAppContext({ - isCurrentWorkspaceManager: true, - isCurrentWorkspaceOwner: false, - })) - - vi.mocked(useReferenceSettings).mockReturnValue({ - data: { - permission: { - install_permission: PermissionType.admin, - debug_permission: PermissionType.admin, - }, - }, - } as ReturnType) - - const { result } = renderHook(() => useReferenceSetting()) - - expect(result.current.canInstall).toBe(true) - expect(result.current.canDebugger).toBe(true) - }) - - it('should return isAdmin when permission is admin and user is owner', () => { - vi.mocked(useAppContext).mockReturnValue(createAppContext({ - isCurrentWorkspaceManager: false, - isCurrentWorkspaceOwner: true, - })) - - vi.mocked(useReferenceSettings).mockReturnValue({ - data: { - permission: { - install_permission: PermissionType.admin, - debug_permission: PermissionType.admin, - }, - }, - } as ReturnType) - - const { result } = renderHook(() => useReferenceSetting()) - - expect(result.current.canInstall).toBe(true) - expect(result.current.canDebugger).toBe(true) - }) - - it('should return false when permission is admin and user is not admin', () => { - vi.mocked(useAppContext).mockReturnValue(createAppContext({ - isCurrentWorkspaceManager: false, - isCurrentWorkspaceOwner: false, - })) - - vi.mocked(useReferenceSettings).mockReturnValue({ - data: { - permission: { - install_permission: PermissionType.admin, - debug_permission: PermissionType.admin, - }, - }, - } as ReturnType) - - const { result } = renderHook(() => useReferenceSetting()) - - expect(result.current.canInstall).toBe(false) + expect(result.current.canUpdate).toBe(true) + expect(result.current.canViewInstalledPlugins).toBe(true) + expect(result.current.canManagePlugin).toBe(false) + expect(result.current.canUninstall).toBe(false) expect(result.current.canDebugger).toBe(false) }) @@ -178,11 +112,12 @@ describe('useReferenceSetting Hook', () => { expect(result.current.canViewInstalledPlugins).toBe(true) expect(result.current.canManagePlugin).toBe(true) expect(result.current.canUninstall).toBe(true) + expect(result.current.canDebugger).toBe(false) }) - it('should allow install and update but not manage with plugin.install only', () => { + it('should allow install and debug when both plugin.install and plugin.debug are present', () => { vi.mocked(useAppContext).mockReturnValue(createAppContext({ - workspacePermissionKeys: ['plugin.install'], + workspacePermissionKeys: ['plugin.install', 'plugin.debug'], })) const { result } = renderHook(() => useReferenceSetting()) @@ -191,6 +126,7 @@ describe('useReferenceSetting Hook', () => { expect(result.current.canUpdate).toBe(true) expect(result.current.canViewInstalledPlugins).toBe(true) expect(result.current.canManagePlugin).toBe(false) + expect(result.current.canDebugger).toBe(true) }) it('should not allow uninstall with legacy plugin.uninstall only', () => { @@ -316,8 +252,9 @@ describe('useReferenceSetting Hook', () => { const { result } = renderHook(() => useReferenceSetting()) - expect(result.current.canInstall).toBe(false) - expect(result.current.canDebugger).toBe(false) + expect(result.current.referenceSetting).toBeNull() + expect(result.current.canInstall).toBe(true) + expect(result.current.canDebugger).toBe(true) }) }) }) @@ -365,14 +302,9 @@ describe('useCanInstallPluginFromMarketplace Hook', () => { }) it('should return false when canInstall is false', () => { - vi.mocked(useReferenceSettings).mockReturnValue({ - data: { - permission: { - install_permission: PermissionType.noOne, - debug_permission: PermissionType.noOne, - }, - }, - } as ReturnType) + vi.mocked(useAppContext).mockReturnValue(createAppContext({ + workspacePermissionKeys: [], + })) const { result } = renderHook(() => useCanInstallPluginFromMarketplace(), { systemFeatures: { enable_marketplace: true }, diff --git a/web/app/components/plugins/plugin-page/debug-info.tsx b/web/app/components/plugins/plugin-page/debug-info.tsx index 904ea8e025..59e9e0fb37 100644 --- a/web/app/components/plugins/plugin-page/debug-info.tsx +++ b/web/app/components/plugins/plugin-page/debug-info.tsx @@ -2,10 +2,6 @@ import type { FC } from 'react' import { Button } from '@langgenius/dify-ui/button' import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' -import { - RiArrowRightUpLine, - RiBugLine, -} from '@remixicon/react' import * as React from 'react' import { useTranslation } from 'react-i18next' import { useDocLink } from '@/context/i18n' @@ -28,7 +24,7 @@ const DebugInfo: FC = () => { if (!info) { return ( ) } @@ -38,7 +34,7 @@ const DebugInfo: FC = () => { - + )} /> @@ -57,7 +53,7 @@ const DebugInfo: FC = () => { className="flex cursor-pointer items-center gap-0.5 text-text-accent-light-mode-only" > {t(`${i18nPrefix}.viewDocs`, { ns: 'plugin' })} - +
diff --git a/web/app/components/plugins/plugin-page/use-reference-setting.ts b/web/app/components/plugins/plugin-page/use-reference-setting.ts index 3ba52fb014..bb23a1e113 100644 --- a/web/app/components/plugins/plugin-page/use-reference-setting.ts +++ b/web/app/components/plugins/plugin-page/use-reference-setting.ts @@ -6,27 +6,12 @@ import { useAppContext } from '@/context/app-context' import { systemFeaturesQueryOptions } from '@/service/system-features' import { useInvalidateReferenceSettings, useMutationReferenceSettings, useReferenceSettings } from '@/service/use-plugins' import { hasPermission } from '@/utils/permission' -import { PermissionType } from '../types' - -const hasPluginPermission = (permission: PermissionType | undefined, isAdmin: boolean) => { - if (!permission) - return false - - if (permission === PermissionType.noOne) - return false - - if (permission === PermissionType.everyone) - return true - - return isAdmin -} const useReferenceSetting = () => { const { t } = useTranslation() - const { isCurrentWorkspaceManager, isCurrentWorkspaceOwner, langGeniusVersionInfo, workspacePermissionKeys } = useAppContext() + const { langGeniusVersionInfo, workspacePermissionKeys } = useAppContext() const { data } = useReferenceSettings() - const { permission: permissions } = data || {} const invalidateReferenceSettings = useInvalidateReferenceSettings() const { mutate: updateReferenceSetting, isPending: isUpdatePending } = useMutationReferenceSettings({ onSuccess: () => { @@ -34,13 +19,13 @@ const useReferenceSetting = () => { toast.success(t('api.actionSuccess', { ns: 'common' })) }, }) - const isAdmin = isCurrentWorkspaceManager || isCurrentWorkspaceOwner const canInstallPluginByPermissionKey = hasPermission(workspacePermissionKeys, 'plugin.install') const canUpdatePlugin = hasPermission(workspacePermissionKeys, ['plugin.install', 'plugin.manage']) const canViewInstalledPlugins = canUpdatePlugin const canManagePlugin = hasPermission(workspacePermissionKeys, 'plugin.manage') const canUninstall = canManagePlugin + const canDebugger = hasPermission(workspacePermissionKeys, 'plugin.debug') const canSetPermissions = hasPermission(workspacePermissionKeys, 'plugin.install') const canSetAutoUpdate = hasPermission(workspacePermissionKeys, 'plugin.install') const canSetPreferences = canSetPermissions || canSetAutoUpdate @@ -49,11 +34,11 @@ const useReferenceSetting = () => { referenceSetting: data, setReferenceSettings: updateReferenceSetting, canViewInstalledPlugins, - canInstall: canInstallPluginByPermissionKey && hasPluginPermission(permissions?.install_permission, isAdmin), + canInstall: canInstallPluginByPermissionKey, canUpdate: canUpdatePlugin, canManagePlugin, canUninstall, - canDebugger: hasPluginPermission(permissions?.debug_permission, isAdmin), + canDebugger, canSetPermissions, canSetAutoUpdate, canSetPreferences, diff --git a/web/app/components/plugins/reference-setting-modal/__tests__/index.spec.tsx b/web/app/components/plugins/reference-setting-modal/__tests__/index.spec.tsx index 118f6de00d..1c58649e72 100644 --- a/web/app/components/plugins/reference-setting-modal/__tests__/index.spec.tsx +++ b/web/app/components/plugins/reference-setting-modal/__tests__/index.spec.tsx @@ -9,7 +9,10 @@ import { PermissionType } from '@/app/components/plugins/types' import { AUTO_UPDATE_MODE, AUTO_UPDATE_STRATEGY } from '../auto-update-setting/types' import ReferenceSettingModal from '../index' -const mockSystemFeatures = { enable_marketplace: true } +const mockSystemFeatures = { + enable_marketplace: true, + rbac_enabled: false, +} const render = (ui: ReactElement) => renderWithSystemFeatures(ui, { systemFeatures: mockSystemFeatures }) @@ -37,19 +40,26 @@ vi.mock('@langgenius/dify-ui/dialog', () => ({ // Mock OptionCard component vi.mock('@/app/components/workflow/nodes/_base/components/option-card', () => ({ - default: ({ title, onSelect, selected, className }: { + default: ({ title, onSelect, selected, className, disabled, tooltip }: { title: string onSelect: () => void selected: boolean className?: string + disabled?: boolean + tooltip?: string }) => ( ), })) @@ -124,6 +134,7 @@ describe('reference-setting-modal', () => { beforeEach(() => { vi.clearAllMocks() mockSystemFeatures.enable_marketplace = true + mockSystemFeatures.rbac_enabled = false }) // Label component tests moved to label.spec.tsx @@ -259,6 +270,16 @@ describe('reference-setting-modal', () => { // Assert expect(screen.getByTestId('modal-close'))!.toBeInTheDocument() }) + + it('should disable permission controls with settings permissions tooltip beside titles when RBAC is enabled', () => { + mockSystemFeatures.rbac_enabled = true + + render() + + expect(screen.getAllByLabelText('plugin.privilege.configurePermissionsInSettings')).toHaveLength(2) + expect(screen.queryByText('plugin.privilege.configurePermissionsInSettings')).not.toBeInTheDocument() + expect(screen.getAllByTestId(/option-card/).every(option => option.hasAttribute('disabled'))).toBe(true) + }) }) describe('State Management', () => { @@ -296,6 +317,16 @@ describe('reference-setting-modal', () => { expect(noOneOptions[0])!.toHaveAttribute('aria-pressed', 'true') }) + it('should not update permission when RBAC disables permission controls', () => { + mockSystemFeatures.rbac_enabled = true + render() + + const noOneOptions = screen.getAllByTestId('option-card-plugin.privilege.noone') + fireEvent.click(noOneOptions[0]!) + + expect(noOneOptions[0])!.toHaveAttribute('aria-pressed', 'false') + }) + it('should initialize with payload auto_upgrade values', () => { // Arrange const payload = createMockReferenceSetting({ diff --git a/web/app/components/plugins/reference-setting-modal/index.tsx b/web/app/components/plugins/reference-setting-modal/index.tsx index d9dec57a24..95f8756a9a 100644 --- a/web/app/components/plugins/reference-setting-modal/index.tsx +++ b/web/app/components/plugins/reference-setting-modal/index.tsx @@ -35,18 +35,19 @@ const PluginSettingModal: FC = ({ const { auto_upgrade: autoUpdateConfig, permission: privilege } = payload || {} const [tempPrivilege, setTempPrivilege] = useState(privilege) const [tempAutoUpdateConfig, setTempAutoUpdateConfig] = useState(autoUpdateConfig || autoUpdateDefaultValue) - const { data: enable_marketplace } = useSuspenseQuery({ - ...systemFeaturesQueryOptions(), - select: s => s.enable_marketplace, - }) + const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions()) + const isPermissionDisabledByRBAC = systemFeatures.rbac_enabled + const permissionDisabledTip = t(`${i18nPrefix}.configurePermissionsInSettings`, { ns: 'plugin' }) const handlePrivilegeChange = useCallback((key: string) => { return (value: PermissionType) => { + if (isPermissionDisabledByRBAC) + return setTempPrivilege({ ...tempPrivilege, [key]: value, }) } - }, [tempPrivilege]) + }, [isPermissionDisabledByRBAC, tempPrivilege]) const handleSave = useCallback(async () => { await onSave({ @@ -78,7 +79,10 @@ const PluginSettingModal: FC = ({ { title: t(`${i18nPrefix}.whoCanDebug`, { ns: 'plugin' }), key: 'debug_permission', value: tempPrivilege?.debug_permission || PermissionType.noOne }, ].map(({ title, key, value }) => (
-