fix: respect legacy plugin permissions without RBAC (#37903)

This commit is contained in:
Jingyi 2026-06-24 18:18:40 -07:00 committed by GitHub
parent 2483c091aa
commit d349e892f4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 150 additions and 16 deletions

View File

@ -5,7 +5,10 @@ import { hasPermission } from '@/utils/permission'
const pluginReadAndUpdatePermissionKeys = ['plugin.install', 'plugin.manage']
const useWorkspacePluginInstallPermission = () => {
const { langGeniusVersionInfo, workspacePermissionKeys } = useAppContext()
const {
langGeniusVersionInfo,
workspacePermissionKeys,
} = useAppContext()
const canInstallPlugin = useMemo(() => {
return hasPermission(workspacePermissionKeys, 'plugin.install')

View File

@ -164,7 +164,7 @@ describe('useReferenceSetting Hook', () => {
expect(result.current.canDebugger).toBe(false)
})
it('should use plugin keys even when legacy admin permission is configured', () => {
it('should use plugin keys even when legacy admin permission is configured and RBAC is enabled', () => {
vi.mocked(useAppContext).mockReturnValue({
isCurrentWorkspaceManager: false,
isCurrentWorkspaceOwner: false,
@ -179,11 +179,40 @@ describe('useReferenceSetting Hook', () => {
},
} as ReturnType<typeof usePluginPermissionSettings>)
const { result } = renderHook(() => useReferenceSetting(PluginCategoryEnum.tool))
const { result } = renderHook(() => useReferenceSetting(PluginCategoryEnum.tool), {
systemFeatures: { rbac_enabled: true },
})
expect(result.current.canManagement).toBe(true)
expect(result.current.canDebugger).toBe(true)
})
it('should apply legacy noOne plugin permissions when RBAC is disabled', () => {
vi.mocked(useAppContext).mockReturnValue({
isCurrentWorkspaceManager: true,
isCurrentWorkspaceOwner: false,
langGeniusVersionInfo: { current_version: '1.0.0', latest_version: '', version: '' },
workspacePermissionKeys: ['plugin.install', 'plugin.manage', 'plugin.debug'],
} as ReturnType<typeof useAppContext>)
vi.mocked(usePluginPermissionSettings).mockReturnValue({
data: {
install_permission: PermissionType.noOne,
debug_permission: PermissionType.noOne,
},
} as ReturnType<typeof usePluginPermissionSettings>)
const { result } = renderHook(() => useReferenceSetting(PluginCategoryEnum.tool), {
systemFeatures: { rbac_enabled: false },
})
expect(result.current.canInstallPlugin).toBe(false)
expect(result.current.canManagement).toBe(false)
expect(result.current.canUpdatePlugin).toBe(false)
expect(result.current.canViewInstalledPlugins).toBe(true)
expect(result.current.canManagePlugin).toBe(false)
expect(result.current.canDebugPlugin).toBe(false)
expect(result.current.canDebugger).toBe(false)
})
})
describe('canSetPermissions', () => {
@ -446,15 +475,33 @@ describe('useCanInstallPluginFromMarketplace Hook', () => {
expect(result.current.canInstallPluginFromMarketplace).toBe(false)
})
it('should not fetch legacy plugin permissions or category auto-upgrade settings', () => {
it('should fetch legacy plugin permissions but not category auto-upgrade settings', () => {
renderHook(() => useCanInstallPluginFromMarketplace(), {
systemFeatures: { enable_marketplace: true },
})
expect(usePluginPermissionSettings).not.toHaveBeenCalled()
expect(usePluginPermissionSettings).toHaveBeenCalled()
expect(usePluginAutoUpgradeSettings).not.toHaveBeenCalled()
})
it('should return false when legacy install permission is noOne and RBAC is disabled', () => {
vi.mocked(usePluginPermissionSettings).mockReturnValue({
data: {
install_permission: PermissionType.noOne,
debug_permission: PermissionType.everyone,
},
} as ReturnType<typeof usePluginPermissionSettings>)
const { result } = renderHook(() => useCanInstallPluginFromMarketplace(), {
systemFeatures: {
enable_marketplace: true,
rbac_enabled: false,
},
})
expect(result.current.canInstallPluginFromMarketplace).toBe(false)
})
it('should use plugin.install when marketplace and RBAC are enabled', () => {
vi.mocked(useAppContext).mockReturnValue({
isCurrentWorkspaceManager: false,

View File

@ -7,6 +7,7 @@ import { useAppContext } from '@/context/app-context'
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
import { useInvalidateReferenceSettings, useMutationPluginPermissionSettings, useMutationReferenceSettings, usePluginAutoUpgradeSettings, usePluginPermissionSettings } from '@/service/use-plugins'
import { hasPermission } from '@/utils/permission'
import { hasLegacyPluginPermissionAccess } from '../plugin-permissions'
const pluginReadAndUpdatePermissionKeys = ['plugin.install', 'plugin.manage']
@ -26,7 +27,11 @@ const useCanSetPluginSettings = () => {
export const usePluginSettingsAccess = () => {
const { t } = useTranslation()
const { workspacePermissionKeys, langGeniusVersionInfo } = useAppContext()
const { isCurrentWorkspaceManager, isCurrentWorkspaceOwner, workspacePermissionKeys, langGeniusVersionInfo } = useAppContext()
const { data: rbacEnabled } = useSuspenseQuery({
...systemFeaturesQueryOptions(),
select: s => s.rbac_enabled,
})
const { canSetPermissions, canSetPluginPreferences } = useCanSetPluginSettings()
const permissionQuery = usePluginPermissionSettings()
const { data: permissions } = permissionQuery
@ -35,11 +40,22 @@ export const usePluginSettingsAccess = () => {
toast.success(t('api.actionSuccess', { ns: 'common' }))
},
})
const canInstallPlugin = hasPermission(workspacePermissionKeys, 'plugin.install')
const canUpdatePlugin = hasPermission(workspacePermissionKeys, pluginReadAndUpdatePermissionKeys)
const isAdminOrOwner = isCurrentWorkspaceManager || isCurrentWorkspaceOwner
const legacyCanInstallPlugin = hasLegacyPluginPermissionAccess({
isAdminOrOwner,
permission: permissions?.install_permission,
rbacEnabled,
})
const legacyCanDebugPlugin = hasLegacyPluginPermissionAccess({
isAdminOrOwner,
permission: permissions?.debug_permission,
rbacEnabled,
})
const canInstallPlugin = hasPermission(workspacePermissionKeys, 'plugin.install') && legacyCanInstallPlugin
const canUpdatePlugin = hasPermission(workspacePermissionKeys, pluginReadAndUpdatePermissionKeys) && legacyCanInstallPlugin
const canViewInstalledPlugins = hasPermission(workspacePermissionKeys, pluginReadAndUpdatePermissionKeys)
const canManagePlugin = hasPermission(workspacePermissionKeys, 'plugin.manage')
const canDebugPlugin = hasPermission(workspacePermissionKeys, 'plugin.debug')
const canManagePlugin = hasPermission(workspacePermissionKeys, 'plugin.manage') && legacyCanInstallPlugin
const canDebugPlugin = hasPermission(workspacePermissionKeys, 'plugin.debug') && legacyCanDebugPlugin
return {
permission: permissions,
@ -102,12 +118,18 @@ const useReferenceSetting = (category: PluginCategoryEnum) => {
}
export const useCanInstallPluginFromMarketplace = () => {
const { data: marketplaceAccess } = useSuspenseQuery({
...systemFeaturesQueryOptions(),
select: s => s.enable_marketplace,
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
const marketplaceAccess = systemFeatures.enable_marketplace
const rbacEnabled = systemFeatures.rbac_enabled
const { isCurrentWorkspaceManager, isCurrentWorkspaceOwner, workspacePermissionKeys } = useAppContext()
const permissionQuery = usePluginPermissionSettings()
const { data: permissions } = permissionQuery
const legacyCanInstallPlugin = hasLegacyPluginPermissionAccess({
isAdminOrOwner: isCurrentWorkspaceManager || isCurrentWorkspaceOwner,
permission: permissions?.install_permission,
rbacEnabled,
})
const { workspacePermissionKeys } = useAppContext()
const canInstallPlugin = hasPermission(workspacePermissionKeys, 'plugin.install')
const canInstallPlugin = hasPermission(workspacePermissionKeys, 'plugin.install') && legacyCanInstallPlugin
const canInstallPluginFromMarketplace = useMemo(() => {
return Boolean(marketplaceAccess && canInstallPlugin)

View File

@ -0,0 +1,27 @@
import { PermissionType } from './types'
type LegacyPluginPermissionAccessOptions = {
isAdminOrOwner: boolean
permission?: PermissionType
rbacEnabled?: boolean
}
export const hasLegacyPluginPermissionAccess = ({
isAdminOrOwner,
permission,
rbacEnabled,
}: LegacyPluginPermissionAccessOptions) => {
if (rbacEnabled !== false)
return true
if (!permission)
return false
if (permission === PermissionType.everyone)
return true
if (permission === PermissionType.admin)
return isAdminOrOwner
return false
}

View File

@ -15,6 +15,10 @@ const { mockRouterPush } = vi.hoisted(() => ({
mockRouterPush: vi.fn(),
}))
const { mockCanInstallPlugin } = vi.hoisted(() => ({
mockCanInstallPlugin: vi.fn(() => true),
}))
const listRenderSpy = vi.fn()
vi.mock('@/app/components/plugins/marketplace/list', () => ({
default: (props: {
@ -30,6 +34,12 @@ vi.mock('@/app/components/plugins/marketplace/list', () => ({
},
}))
vi.mock('@/app/components/plugins/plugin-page/use-reference-setting', () => ({
usePluginSettingsAccess: () => ({
canInstallPlugin: mockCanInstallPlugin(),
}),
}))
const mockUseMarketplaceCollectionsAndPlugins = vi.fn()
const mockUseMarketplacePlugins = vi.fn()
vi.mock('@/app/components/plugins/marketplace/hooks', () => ({
@ -108,6 +118,7 @@ const createMarketplaceContext = (overrides: Partial<ReturnType<typeof useMarket
describe('Marketplace', () => {
beforeEach(() => {
vi.clearAllMocks()
mockCanInstallPlugin.mockReturnValue(true)
})
// Rendering the marketplace panel based on loading and visibility state.
@ -154,6 +165,28 @@ describe('Marketplace', () => {
showInstallButton: true,
}))
})
it('should hide install actions when plugin install permission is missing', () => {
mockCanInstallPlugin.mockReturnValue(false)
const marketplaceContext = createMarketplaceContext({
isLoading: false,
plugins: [createPlugin()],
})
render(
<Marketplace
searchPluginText=""
filterPluginTags={[]}
isMarketplaceArrowVisible={false}
showMarketplacePanel={vi.fn()}
marketplaceContext={marketplaceContext}
/>,
)
expect(listRenderSpy).toHaveBeenCalledWith(expect.objectContaining({
showInstallButton: false,
}))
})
})
// Prop-driven UI output such as links and action triggers.

View File

@ -11,6 +11,7 @@ import { useTranslation } from 'react-i18next'
import { useLocale } from '#i18n'
import Loading from '@/app/components/base/loading'
import List from '@/app/components/plugins/marketplace/list'
import { usePluginSettingsAccess } from '@/app/components/plugins/plugin-page/use-reference-setting'
import { useRouter } from '@/next/navigation'
import { getMarketplaceUrl } from '@/utils/var'
import { toolsContentInsetClassNames, toolsUnifiedContentFrameClassName } from '../content-inset'
@ -35,6 +36,7 @@ const Marketplace = ({
const { t } = useTranslation()
const { theme } = useTheme()
const router = useRouter()
const { canInstallPlugin } = usePluginSettingsAccess()
const {
isLoading,
marketplaceCollections,
@ -127,7 +129,7 @@ const Marketplace = ({
marketplaceCollections={marketplaceCollections || []}
marketplaceCollectionPluginsMap={marketplaceCollectionPluginsMap || {}}
plugins={plugins}
showInstallButton
showInstallButton={canInstallPlugin}
cardContainerClassName={cardContainerClassName}
onCollectionMoreClick={handleCollectionMoreClick}
/>