fix: validate plugin installation scope (#39669)

This commit is contained in:
yyh
2026-07-28 04:13:20 +00:00
committed by GitHub
parent dd28b0d165
commit 1e5e47b889
6 changed files with 289 additions and 171 deletions
@@ -4,125 +4,101 @@ import { InstallationScope } from '@/features/system-features/constants'
import { renderHookWithConsoleQuery as renderHook } from '@/test/console/query-data'
import { pluginInstallLimit } from '../use-install-plugin-limit'
type PluginInstallCandidate = Parameters<typeof pluginInstallLimit>[0]
type SystemFeatures = Parameters<typeof pluginInstallLimit>[1]
const basePlugin = {
from: 'marketplace' as const,
verification: { authorized_category: 'langgenius' },
} satisfies PluginInstallCandidate
function makeSystemFeatures(
scope: PluginInstallationScope,
restrictToMarketplaceOnly = false,
): SystemFeatures {
return {
plugin_installation_permission: {
restrict_to_marketplace_only: restrictToMarketplaceOnly,
plugin_installation_scope: scope,
},
}
}
describe('pluginInstallLimit', () => {
it('should allow all plugins when scope is ALL', () => {
const features = {
plugin_installation_permission: {
restrict_to_marketplace_only: false,
plugin_installation_scope: InstallationScope.ALL,
},
}
const features = makeSystemFeatures(InstallationScope.ALL)
expect(pluginInstallLimit(basePlugin as never, features as never).canInstall).toBe(true)
expect(pluginInstallLimit(basePlugin, features).canInstall).toBe(true)
})
it('should deny all plugins when scope is NONE', () => {
const features = {
plugin_installation_permission: {
restrict_to_marketplace_only: false,
plugin_installation_scope: InstallationScope.NONE,
},
}
const features = makeSystemFeatures(InstallationScope.NONE)
expect(pluginInstallLimit(basePlugin as never, features as never).canInstall).toBe(false)
expect(pluginInstallLimit(basePlugin, features).canInstall).toBe(false)
})
it('should allow langgenius plugins when scope is OFFICIAL_ONLY', () => {
const features = {
plugin_installation_permission: {
restrict_to_marketplace_only: false,
plugin_installation_scope: InstallationScope.OFFICIAL_ONLY,
},
}
const features = makeSystemFeatures(InstallationScope.OFFICIAL_ONLY)
expect(pluginInstallLimit(basePlugin as never, features as never).canInstall).toBe(true)
expect(pluginInstallLimit(basePlugin, features).canInstall).toBe(true)
})
it('should deny non-official plugins when scope is OFFICIAL_ONLY', () => {
const features = {
plugin_installation_permission: {
restrict_to_marketplace_only: false,
plugin_installation_scope: InstallationScope.OFFICIAL_ONLY,
},
}
const plugin = { ...basePlugin, verification: { authorized_category: 'community' } }
const features = makeSystemFeatures(InstallationScope.OFFICIAL_ONLY)
const plugin = {
...basePlugin,
verification: { authorized_category: 'community' as const },
} satisfies PluginInstallCandidate
expect(pluginInstallLimit(plugin as never, features as never).canInstall).toBe(false)
expect(pluginInstallLimit(plugin, features).canInstall).toBe(false)
})
it('should allow partner plugins when scope is OFFICIAL_AND_PARTNER', () => {
const features = {
plugin_installation_permission: {
restrict_to_marketplace_only: false,
plugin_installation_scope: InstallationScope.OFFICIAL_AND_PARTNER,
},
}
const plugin = { ...basePlugin, verification: { authorized_category: 'partner' } }
const features = makeSystemFeatures(InstallationScope.OFFICIAL_AND_PARTNER)
const plugin = {
...basePlugin,
verification: { authorized_category: 'partner' as const },
} satisfies PluginInstallCandidate
expect(pluginInstallLimit(plugin as never, features as never).canInstall).toBe(true)
expect(pluginInstallLimit(plugin, features).canInstall).toBe(true)
})
it('should deny github plugins when restrict_to_marketplace_only is true', () => {
const features = {
plugin_installation_permission: {
restrict_to_marketplace_only: true,
plugin_installation_scope: InstallationScope.ALL,
},
}
const plugin = { ...basePlugin, from: 'github' as const }
const features = makeSystemFeatures(InstallationScope.ALL, true)
const plugin = { ...basePlugin, from: 'github' as const } satisfies PluginInstallCandidate
expect(pluginInstallLimit(plugin as never, features as never).canInstall).toBe(false)
expect(pluginInstallLimit(plugin, features).canInstall).toBe(false)
})
it('should deny package plugins when restrict_to_marketplace_only is true', () => {
const features = {
plugin_installation_permission: {
restrict_to_marketplace_only: true,
plugin_installation_scope: InstallationScope.ALL,
},
}
const plugin = { ...basePlugin, from: 'package' as const }
const features = makeSystemFeatures(InstallationScope.ALL, true)
const plugin = { ...basePlugin, from: 'package' as const } satisfies PluginInstallCandidate
expect(pluginInstallLimit(plugin as never, features as never).canInstall).toBe(false)
expect(pluginInstallLimit(plugin, features).canInstall).toBe(false)
})
it('should allow marketplace plugins even when restrict_to_marketplace_only is true', () => {
const features = {
plugin_installation_permission: {
restrict_to_marketplace_only: true,
plugin_installation_scope: InstallationScope.ALL,
},
}
const features = makeSystemFeatures(InstallationScope.ALL, true)
expect(pluginInstallLimit(basePlugin as never, features as never).canInstall).toBe(true)
expect(pluginInstallLimit(basePlugin, features).canInstall).toBe(true)
})
it('should default to langgenius when no verification info', () => {
const features = {
plugin_installation_permission: {
restrict_to_marketplace_only: false,
plugin_installation_scope: InstallationScope.OFFICIAL_ONLY,
},
}
const plugin = { from: 'marketplace' as const }
const features = makeSystemFeatures(InstallationScope.OFFICIAL_ONLY)
const plugin = { from: 'marketplace' as const } satisfies PluginInstallCandidate
expect(pluginInstallLimit(plugin as never, features as never).canInstall).toBe(true)
expect(pluginInstallLimit(plugin, features).canInstall).toBe(true)
})
it('should fallback to canInstall true for unrecognized scope', () => {
it('should deny installation for an unrecognized runtime scope', () => {
const features = {
plugin_installation_permission: {
restrict_to_marketplace_only: false,
plugin_installation_scope: 'unknown-scope' as unknown as PluginInstallationScope,
plugin_installation_scope: 'unknown-scope',
},
}
} as unknown as SystemFeatures
expect(pluginInstallLimit(basePlugin as never, features as never).canInstall).toBe(true)
expect(pluginInstallLimit(basePlugin, features).canInstall).toBe(false)
})
})
@@ -132,9 +108,9 @@ describe('usePluginInstallLimit', () => {
const plugin = {
from: 'marketplace' as const,
verification: { authorized_category: 'langgenius' },
}
} satisfies PluginInstallCandidate
const { result } = renderHook(() => usePluginInstallLimit(plugin as never))
const { result } = renderHook(() => usePluginInstallLimit(plugin))
expect(result.current.canInstall).toBe(true)
})
@@ -1,68 +1,55 @@
import type { GetSystemFeaturesResponse } from '@dify/contracts/api/console/system-features/types.gen'
import type { Plugin, PluginManifestInMarket } from '../../types'
import type {
PluginBundleDependencyType,
PluginVerification,
} from '@dify/contracts/api/console/workspaces/types.gen'
import { useSuspenseQuery } from '@tanstack/react-query'
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
import { InstallationScope } from '@/features/system-features/constants'
type PluginProps = (Plugin | PluginManifestInMarket) & {
from: 'github' | 'marketplace' | 'package'
type PluginInstallCandidate = {
from: PluginBundleDependencyType
verification?: PluginVerification | null
}
type PluginInstallLimitResult = {
canInstall: boolean
}
function denyUnsupportedInstallationScope(_scope: never): PluginInstallLimitResult {
return { canInstall: false }
}
export function pluginInstallLimit(
plugin: PluginProps,
plugin: PluginInstallCandidate,
systemFeatures: Pick<GetSystemFeaturesResponse, 'plugin_installation_permission'>,
) {
if (systemFeatures.plugin_installation_permission.restrict_to_marketplace_only) {
const permission = systemFeatures.plugin_installation_permission
if (permission.restrict_to_marketplace_only) {
if (plugin.from === 'github' || plugin.from === 'package') return { canInstall: false }
}
if (
systemFeatures.plugin_installation_permission.plugin_installation_scope ===
InstallationScope.ALL
) {
return {
canInstall: true,
}
}
if (
systemFeatures.plugin_installation_permission.plugin_installation_scope ===
InstallationScope.NONE
) {
return {
canInstall: false,
}
}
const verification = plugin.verification || {}
if (!plugin.verification || !plugin.verification.authorized_category)
verification.authorized_category = 'langgenius'
const authorizedCategory = plugin.verification?.authorized_category ?? 'langgenius'
const scope = permission.plugin_installation_scope
if (
systemFeatures.plugin_installation_permission.plugin_installation_scope ===
InstallationScope.OFFICIAL_ONLY
) {
return {
canInstall: verification.authorized_category === 'langgenius',
}
}
if (
systemFeatures.plugin_installation_permission.plugin_installation_scope ===
InstallationScope.OFFICIAL_AND_PARTNER
) {
return {
canInstall:
verification.authorized_category === 'langgenius' ||
verification.authorized_category === 'partner',
}
}
return {
canInstall: true,
switch (scope) {
case InstallationScope.ALL:
return { canInstall: true }
case InstallationScope.NONE:
return { canInstall: false }
case InstallationScope.OFFICIAL_ONLY:
return { canInstall: authorizedCategory === 'langgenius' }
case InstallationScope.OFFICIAL_AND_PARTNER:
return {
canInstall: authorizedCategory === 'langgenius' || authorizedCategory === 'partner',
}
default:
return denyUnsupportedInstallationScope(scope)
}
}
export default function usePluginInstallLimit(plugin: PluginProps): PluginInstallLimitResult {
export default function usePluginInstallLimit(
plugin: PluginInstallCandidate,
): PluginInstallLimitResult {
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
return pluginInstallLimit(plugin, systemFeatures)